Back to site
Since 2004, our University project has become the Internet's most widespread web hosting directory. Here we like to talk a lot about web servers, web development, networking and security services. It is, after all, our expertise. To make things better we've launched this science section with the free access to educational resources and important scientific material translated to different languages.

Testiranje Backbone aplikacije

Testranje Backbone aplikacija alatima Jasmine i Sinon – Prvi deo

Pregled

Ovo je prvi u seriji članaka koji prikazuju kako se testira Backbone.js aplikacija, uz pomoć Jasmine BDD testnog okvira i Sinon.JS biblioteke za lažne objekte (spies, stubs i mocks).

U ovom delu, ukratko ćemo prikazati Backbone, a zatim ćemo predstaviti neke od karakterističnih osobina koje imaju Jasmine i Sinon.JS. Usput ćemo pogledati zašto su ovi alati tako pogodni za testiranje Backbone aplikacija .

Ako nikad ranije niste pisali JavaScript testove možda ćete hteti da pogledate najnovije serije članaka koje je napisao Christian Johansen na scriptjunkie.

Backbone svuda

Poslednjih nekoliko meseci, Backbone.js je bio prilično izložen očima javnosti, zahvaljujući brojnim tutorijalima i nekim implementacijama visokih profila. .

Popularnost koju Backbone uživa je razumljiva. Pruža minimalnu model-view-controller MVC stukturu da bi pomogao organizovanje kompleksnog koda, ali programeru ostavlja i druge mogućnosti izbora. Za razliku od bogatog JavaScript UI okvira, kakav je Cappuccino, ne obezbeđuje UI dodatke ili teme, ali prepušta programeru izbor DOM biblioteke. Backbone ima specifičnu podršku za jQuery ili Zepto ali ne isključuje upotrebu drugih biblioteka.

Backbone MVC struktura odlično se uklapa u integraciono testiranje jedinice. Podela na modele, kolekcije, poglede i rutere znači da se ponašanje svake ‘klase’ (jedinice) može izolovano testirati, unapred eliminišući neke greške i znatno olakšavajući proces uklanjanja grešaka. .

Ovo bi trebalo da bude dobro poznato svakome ko je proveo bar neko vreme testirajući aplikacije razvijene MVC okvirima, kao što su Rails ili Django. Postoji niz zrelih biblioteka, alata i pristupa dizajniranih za testiranje jedinica ove aplikacije. Moraćete da napišete JavaScript testove kako bi vaš aplikacijski kod na strani klijenta bio istog kvaliteta kao i vaš kod na strani servera.

O Jasmine BDD-u

Jasmine je (BDD) testni okvir za JavaScript, koji je razvijen na osnovu Ruby biblioteke RSpec. Kao i kod RSpec, Jasmine vam omogućava da pišete ‘specifikacije’ (ne testove) koji predstavljaju pojedinačne primere ponašanja kakve želite da vaš kod ispolji .

BDD naglašava zajednički jezik među programerima i zainteresovanim stranama. BDD zajednica i način na koji se pišu specifikacije u RSpec i Jasmine okvirima podstiče programere da se koncentrišu na spoljašnje ponašanje svog koda, umesto na unutrašnje detalje, sa specifikacijama formulisanim u smislu ovog zajedničkog jezika. To ohrabruje programera da uvek razmatra korisnu vrednost funkcije koja se razvija i da se usmeri na njeno isporučivanje.

Ako ste zainteresovani da naučite nešto više o BDD-u, pogledajte originalni članak o BDD-u koji je napisao Dan North, ili RSpec Book u kojoj ima nekoliko poglavlja posvećenih ovoj temi. Ta knjiga više predstavlja BDD Bibliju, nego priručnik za RSpec..

Specifikacije

Hajde da prokopamo dublje i vidimo kako izgledaju specifikacije za Jasmine. Evo jednog pojednostavljenog primera specifikacija za Backbone model koji koristi Jasmine:

it("should expose an attribute", function() {
  var episode = new Backbone.Model({
    title: "Hollywood - Part 2"
  });
  expect(episode.get("title"))
    .toEqual("Hollywood - Part 2");
});

Specifikacija jednostavno označava opis očekivanog ponašanja, koda, čiji će rezultat biti to ponašanje i jedno ili više očekivanja koje testiraju ponašanje.

Pojedinačne specifikacije treba da budu kratke i da testiraju samo jedan aspekt ponašanja. Ako zateknete sebe kako zapisujete veliki broj različitih očekivanja ili ako specifikacije postanu veoma dugačke, razmislite o tome da ih razbijete u druge specifikacije. Grupisanje specifikacija nizovima, upotreba zajedničkih postavki i funkcije teardown mogu pomoći u vezi s tim.

Paketi

Specifikacije su grupisane u pakete (Suites) koji su određeni funkcijom describe() . Na primer, sve specifikacije modela Episode mogu se grupisati u pakete na sledeći način:

describe("Episode model", function() {
  it("should expose an attribute", function() {
  ...
  });
  it("should validate on save", function() {
  ...
  });
});

Paketi, takođe, mogu biti ugnežđeni, što je super kad imate veliki broj specifikacija, jer ih možete organizovati u pakete diskretnih parčića. Volim da koristim describe describe blok da objedinim specifikacije koje se odnose na određeni početni kontekst. Ovako se bolje zadržava konverzacijski stil specifikacija. Na primer:

describe("Episode model", function() {
  describe("when creating a new episode", function() {
    it("should expose the title attribute", function() {
      ...
    });
    it("should have a default parental rating", function() {
      ...
    });
  });
});

beforeEach() and afterEach()

Kao u tradicionalnim okvirima za testiranje xUnit stila, po želji, možete odrediti kod koji će se pokretati pre i posle svakog testa. To je odlično za obezbeđivanje konzistentnih uslova za svaki test, kao i za postavljanje promenljivih i objekata koji se koriste u vašim specifikacijama.

Primer prikazan na donjoj slici koristi beforeEach() da kreira primer modela koji će se koristiti u svakoj specifikaciji.

describe("Episode", function() {
  
  beforeEach(function() {
    this.episode = new Backbone.Model({
      title: "Hollywood - Part 2"
    });
  });
  
  it("should expose an attribute", function() {
    expect(this.episode.get("title"))
      .toEqual("Hollywood - Part 2");
  });
    
  it("should validate on save", function() {
    ...
  });
    
});

Možete obezbediti beforeEach() i afterEach() metod za svaki ugnežđeni describe koji imate u svojim specifikacijama. To vam omogućava da imate i opšte i specifične metode postavke, kao i metode brisanja krojene za svaki paket specifikacija. Kao što ćete videti u drugim delovima ovog članka, ovo je veoma zgodno za smanjenje ponavljanja i kontrolisanje preciznih uslova za svaku specifikaciju.

Pokretač specifikacija

Ova struktura daje specifikacije koje su drugim programerima veoma lake za čitanje i direktnu interpretaciju, uglavnom zbog opisa svake specifikacije i formata tragača očekivanja podudarnosti (expectation matchers) . .

Jasmine takođe pruža jednostavan pokretač specifikacija koji je, jednostavno, HTML stranica sa skriptom koji će pokrenuti sve specifikacije koje unesete. Sledeća slika prikazuje izlaz iz paketa specifikacija gde jedna specifikacija nije izvršena:

An example Jasmine spec runner output

U ovom članku predstavićemo i neke druge korisne osobine koje ima Jasmine, prema potrebi, uključujući stvaranje fikstura i vaših prilagođenih expectation matcher-a. Sada, bacimo se na Sinon.JS .

Sinon.JS

Sinon.JS je biblioteka koja pruža lažne objekte (spies, stubs i mocks) za testiranje vašeg JavaScript koda. Ako ne znate šta je to, niste jedini. Upotreba ovih konstrukata u testiranju JavaScript koda još nije potpuno odomaćena. Međutim, ako programirate bogatu, složenu aplikaciju kao što su one za koje se koristi Backbone, lažni objekti predstavljaju veoma koristan deo kompleta alata za testiranje.

Christian Johansen, koji je kreirao Sinon.JS, objašnjava zašto biste poželeli da koristite lažnjake u još jednom scriptjunkie članku. U JavaScript aplikacijama, ti razlozi svode se na:

  1. Performanse - pravo DOM upravljanje, oslanjanje na pravovremeno ponašanje i aktivnosti na mreži usporavaju testove;
  2. Izolaciju - testovi jedinice treba da se usmere na što manji deo funkcionalnosti, koliko god je to moguće i da budu oslobođeni nepouzdahih i sporih zavisnosti.

Upotreba lažnih objekata je osnovni deo prihvatanja programiranja vođenog testiranjem i ponašanjem. U suštini, lažni objekti omogućavaju testiranje koda izolovano od svojih zavisnosti. Bilo koji API ili zavisni moduli, koje kodirate u testu, mogu biti lažirani tako da odgovaraju potrebama vašeg testa. Možete pratiti lažne metode da biste tačno videli kako se pozivaju tokom testa.

Sinon.JS omogućava vam da napravite lažne objekte gotovo za sve. Možete napraviti lažne delove za sopstvenu aplikaciju, određene načine ponašanja u okviru jQuery-ja, sam XmlHttpRequest API, ili možete čak lažirati metode timer-a za JavaScript da biste omogućili rapidno testiranje koda koje zavisi od vremenske usklađenosti, kao što su animacije i pauze.

Sinon.JS pruža tri vrste lažnih objekata: spies, stubs i mocks.

Spies

Špijuni (spies) su funkcije koje prate kako i koliko često ih pozivate kao i povratne rezultate. Ovo je fenomenalno korisno u aplikacijama na asinhroni pogon jer možete poslati špijuna da prati šta se dešava s vašim metodama, čak i ako su te metode anonimne ili zatvorene od direktne inspekcije.

Špijuni mogu biti "anonimni" ili mogu da špijuniraju postojeće funkcije.

Anonimni špijun je samo prazna funkcija za špijuniranje karakteristika koji se može poslati da snimi kako je ta karakteristika korišćena. Kao pravi špijun poslat iza neprijateljskih linija sa mikrofonom pričvršćenim na grudi, predstavlja najmudriji metod za testiranje. Ovde je primer špijuna koji testira jednostavan, prilagođen Backbone vezivanju događaja:

it("should fire a callback when 'foo' is triggered", function() {
  // Create an anonymous spy
  var spy = sinon.spy();
  
  // Create a new Backbone 'Episode' model
  var episode = new Episode({
    title: "Hollywood - Part 2"
  });
  
  // Call the anonymous spy method when 'foo' is triggered
  episode.bind('foo', spy); 
  
  // Trigger the foo event
  episode.trigger('foo'); 
  
  // Expect that the spy was called at least once
  expect(spy.called).toBeTruthy(); 
});

To će proći ako se špijun poziva zove jednom ili više puta, bez obzira na način poziva i vrstu argumenata. Međutim, Sinon obezbeđuje niz metoda koje vam omogućavaju da, po želji, strogo odredite izgled i broj poziva, kao i odgovor špijuna.

Špijunsko ponašanje može se pripojiti postojećem metodu. Smešno, volim da ih nazivam “krticama”. To je korisno kad treba da proverite da li neki deo funkcionalnosti poziva drugi deo koda na očekivani način. Na primer, možda ćete hteti da proverite da li sačuvani metod modela ispravno poziva jQuery $.ajax .

it("should make the correct server request", function() {
  
  var episode = new Backbone.Model({
    title: "Hollywood - Part 2",
    url: "/episodes/1"
  });
  
  // Spy on jQuery's ajax method
  var spy = sinon.spy(jQuery, 'ajax');
  
  // Save the model
  episode.save();
  
  // Spy was called
  expect(spy).toHaveBeenCalled();
  // Check url property of first argument
  expect(spy.getCall(0).args[0].url)
    .toEqual("/episodes/1");
  
  // Restore jQuery.ajax to normal
  jQuery.ajax.restore();
});

Stubs i Mocks

Stubs (okrajci) i mocks (imitatori) u Sinon-u sprovode sve funkcije špijuna, ali uz neka dodatna zaduženja. Stubs vam omogućavaju da zamenite postojeće ponašanje određenog metoda čime god želite. To je odlično za opažanje izuzetaka i grešaka scenarija iz spoljnih zavisnosti, tako da možete testirati da li će vaš kod reagovati na odgovarajući način. Takođe, omogućava vam da počnete programiranje dok druge zavisnosti još nisu na svojim mestima.

Mocks obezbeđuju sve to, ali umesto toga, zavaravaju ceo API i određuju ugrađena očekivanja o tome kako treba da budu iskorišćeni. Prate način upotrebe kao spies i kao stubs reaguju na unapred programirani način u skladu s potrebama testa. Međutim, za razliku od špijuna, očekivanja za njihovo ponašanje unapred su programirana, a nijedan korak verifikacije na kraju neće uspeti ako bilo koje od pojedinačnih očekivanja ostane neispunjeno.

U ostalim delovima ovog članka istražićemo stubs i mocks prema potrebi.

Lažni Ajax i lažni serveri

Sinon nije ograničen na obične funkcije i metode špijuniranja i lažiranja. Takođe, Sinon pruža prečice za fabrikovanje Ajax odgovora. To znači da možete testirati svoj kod u potpunoj izolaciji od svog JSON izvora podataka i ne zavisite od pokretanja web aplikacije da biste pokrenuli svoje grupe specifikacija. Osim toga, možete testirati da li vaša aplikacija reaguje na odgovarajući način kad skrene sa svog puta, uključujući nevažeće JSON i razne HTTP odzive kodova.

Evo jednostavnog primera specifikacije Backbone modela fetch metoda koji koristi lažni server da odgovori na zahteve Ajax-a:

describe("Episode model", function() {
  beforeEach(function() {
    this.server = sinon.fakeServer.create();
  });
    
  afterEach(function() {
    this.server.restore();
  });

  it("should fire the change event", function() {
    var callback = sinon.spy();
    
    // Set how the fake server will respond
    // This reads: a GET request for /episode/123 
    // will return a 200 response of type 
    // application/json with the given JSON response body
    this.server.respondWith("GET", "/episode/123",
      [200, {"Content-Type": "application/json"},
      '{"id":123,"title":"Hollywood - Part 2"}']);

    var episode = new Episode({id: 123});
    
    // Bind to the change event on the model
    episode.bind('change', callback);
    
    // makes an ajax request to the server
    episode.fetch(); 
    
    // Fake server responds to the request
    this.server.respond(); 
        
    // Expect that the spy was called with the new model
    expect(callback.called).toBeTruthy();
    expect(callback.getCall(0).args[0].attributes)
      .toEqual({
        id: 123,
        title: "Hollywood - Part 2"
      });
    
  });

});

Ova specifikacija može se napraviti da prođe s ovim jednostavnim Backbone modelom:

var Episode = Backbone.Model.extend({
  url: function() {
    return "/episode/" + this.id;
  }
});

Postoji još mnogo toga o Sinon-u što ovde nismo pokrili. Konkretno, lažni timer-i veoma su korisni za testiranje funkcionalnosti zavisne od vremena, kao što su animacije, ali bez usporavanja testova. Pogledajte celokupnu dokumentaciju.

Siže

U svetu najnovijih Backbone aplikacija, kompleksno asinhrono i međuzavisno ponašanje može svakom programeru zadati veliku glavobolju. Backbone pomaže programerima da organizuju svoj kod u male, samostalne modele, kolekcije, poglede i rutere. Ali, ovo je samo polovina bitke. Bez dobro testiranog koda veći broj grešaka ostaće neotkriven, a one koje su otkrivene biće teže za praćenje. Ostali članovi tima mogu vam slučajno slomiti kod ili, jednostavno, pogrešno razumeti njegovu svrhu.

U drugom delu ovog članka, preći ćemo na stvarno testiranje nekih Backbone modela i vremenom ćemo izgraditi jednostavnu radnu aplikaciju s paketom specifikacija koje idu s njim.

Testiranje Backbone aplikacija alatima Jasmine i Sinon – Drugi deo. Modeli i kolekcije

Pregled

Ovo je drugi u seriji članaka koji opisuju kako se testira Backbone.js aplikacija uz pomoć Jasmine BDD testnog okvira i Sinon.JS biblioteke za lažne objekte (spies, stubs i mocks). Ako još niste pročitali prvi deo, pogledajte ga sada.

U ovom delu, pregledaćemo neke primere za testiranje Backbone modela i kolekcija. Usput ćemo predstaviti tehnike koje pomažu da vam Jasmine specifikacije budu brze, jasne i efikasne, uključujući:

  • kako da izolujete svaki od Backbone objekata za testiranje;
  • upotrebu Sinon-ovog lažnog servera za provociranje odgovora na Ajax-ove zahteve;
  • upotrebu špijuna za verifikovanje veza događaja i povratnih poziva.

Predstavljanje primera aplikacije

Koji tutorijal za web aplikacije bi bio kompletan bez primera To Do liste aplikacije? Pošto ne bih voleo da se sukobljavam s opštim trendovima, koristićemo je za svoje primere.

Kreiraćemo Todo Backbone model, s naslovom, statusom prioriteta i onim što je urađeno. Kreiraćemo Backbone kolekciju ovih Todo modela pod nazivom Todos. Kad to uradimo, napisaćemo Jasmine specifikacijama da testiraju njihovo ponašanje.

U trećem delu ove serije članaka, napravićemo ruter i pogledati kako objekti upravljaju URL rutingom i odgovarajućim prikazom HTML-a.

Postavka uzorka aplikacije

Uzorke aplikacije možete naći na GitHub. Pratite README za instrukcije o postavci i pokretanju specifikacija.

Slobodno ih izvrćite, klonirajte i igrajte se njima. To je Rails aplikacija, ali Rails deo aplikacije je minimalan pošto samo služi za JSON odgovore na Backbone aplikaciju. U stvari, ako pokrenete aplikaciju, neće se desiti ništa značajno, osim što bi trebalo da vam omogući upotrebu Rails građe na /todos da biste kreirali Todo modele, a zatim ih doneli koristeći /todos.json.

Pokretanje paketa specifikacija

Da biste pokrenuli svoje Jasmine specifikacije, možete da pokrenete rake jasmine:ci da biste upotrebili Selenium za pokretanje paketa specifikacija ili da pokrenite rake jasmine jasmine da biste pokrenuli Jasmine server. Izlaz u terminalu reći će vam koji URL server je pokrenut. Navigacijom do tog URL-a u pretraživaču pokrenućete paket specifikacija.

Backbone modeli

Backbone modeli mogu se dramatično razlikovati, od jednostavnih do složenih, u zavisnosti od zahteva vaše aplikacije. Ovde ćemo se koncentrisati na neke uobičajene modele zadataka – instanciranje, standardne vrednosti, URL i validaciua.

Primer 1: Osnovno instanciranje

Obično ne bi bilo neophodno stvarno testirati jednostavno ponašanje kao što je instanciranje modela, osim ako ne radite nešto zabavno sa svojim kodom. Lako je zaneti se i početi testiranje svake sitnice samo zato što možete, ali uvek treba da se koncentrišete na testiranje svog koda i izbegnete direktno testiranje zavisnosti.

Kad pišete aplikaciju koristeći Backbone, neizbežno ćete imati pomalo tesno spojeni kod, tako da će vam, možda, biti teško da shvatite šta da testirate i za šta da kreirate lažne objekte. Uostalom, objekti vaše aplikacije obično predstavljaju produžene Backbone objekte. Dobro pravilo glasi da direktno treba testirati samo prošireni Backbone objekat na koji se trenutno koncentrišete. Kad objekat zavisi od metoda ili od drugog objekta, prave se samo lažni API-ji koji su vam potrebni u vezi s tim objektom.

Ovaj predmet najbolje se objašnjava na primeru, pa hajde da nastavimo jednostavnom specifikacijom za kreiranje (instanciranje) novog modela. Da, prvo ćemo napisati specifikacije, gledaćemo njihov strašni sunovrat, a zatim ćemo napisati kod da bismo im obezbedili prolaz u skladu s tradicijom ( red-green-refactor) TDD i BDD.

U Rails projektu, gde koristimo Jasmine gem, nove Jasmine datoteke specifikacija kreiraju se u folderu spec/javascripts Međutim, mogu se kreirati gde god vam je potrebno za projekat sve dok datoteku pokreće Jasmine pokretač specifikacija.

Todo.spec.js:

describe('Todo model', function() {

  describe('when instantiated', function() {
    
    it('should exhibit attributes', function() {
      var todo = new Todo({
        title: 'Rake leaves'
      });
      expect(todo.get('title'))
        .toEqual('Rake leaves');
    });
    
  });
  
});

Pokretanje ove specifikacije rezultira sledećim izlazom:

ReferenceError: Todo is not defined in ... Todo.spec.js (line 6)

Dakle, treba da kreiramo Todo.js. U Rails-u, to odlazi negde u public/javascripts. Volim da kreiram odvojene foldere za modele, kolekcije, rutere, poglede i pomoćnike u svojim Backbone aplikacijama da bi sve bilo dobro organizovano.

Todo.js:

var Todo = Backbone.Model.extend();

Kad se ponovo pokrenu specifikacije, dobija se sledeće:

Finished in 0.03458 seconds
1 example, 0 failures

Da, samo je to bilo potrebno da se proširi standardni Backbone.Model . Backbone upravlja atributima. Zbog toga bi neko mogao reći da ova specifikacija nije neophodna, ali bar proverava da li vam je model raspoloživ i da li ima tačan naziv!

Bacimo se sada na nešto malo korisnije.

Primer 2: Podrazumevane vrednosti

Backbone vam omogućava da podesite podrazumevane vrednosti za svoje modele, ukoliko već nisu određene prilikom instanciranja. Uradićemo to za naše prioritetne Todo vrednosti. Ako korisnik ne odredi prioritet, podrazumevana vrednost biće 3 (moguće vrednosti su 1, 2 i 3).

Treba da napišemo još jednu specifikaciju. Kreiranjem Todo instance za svaki primer, možemo pomeriti taj proces u Jasmine funkciju beforeEach da bismo dobili veću efikasnost.

Todo.spec.js:

beforeEach(function() {
  this.todo = new Todo({
    title: 'Rake leaves'
  });
});

This doesn’t have a priority attribute, so we can now write the spec after the previous one:

it('should set the priority to default', function() {
  expect(this.todo.get('priority')).toEqual(3);
});

When run, the output is:

Expected undefined to equal 3.

Now, to write the (very simple) code:

Todo.js

var Todo = Backbone.Model.extend({
  defaults: {
    'priority': 3
  }
});

Primer 3: URL

Validacije su u vezi s podrazumevanim vrednostima. U Backbone.js-u model je proveren kad se pozovu save() ili set() režimi da promene vrednosti atributa.

Međutim, Backbone će izbaciti izuzetak ako pokušate da sačuvate model bez definisanog url pa pogledajmo prvo to.

Backbone modeli ne moraju da imaju podešene url osobine, pod uslovom da su deo kolekcije koja ima url. Modelov url je url kolekcija roditelja plus id. modela. Ako još uvek nema id atribut, to je ‘nov’ model, pa je url isti kao i URL kolekcija, što je podrazumevano.U našoj aplikaciji, i kasnije u ovom članku, kreiraćemo Todos kolekciju koja će imati url od /todos.Tako,Todo model čiji je id 5 imaće url /todos/5.

Ovaj šablon sledi REST konvencije koje se primenjuju u okviru Rails 3, ali URL modela ili kolekcije može biti podešen na bilo koju vrednost niza ili može iskoristiti funkciju da ga generiše svaki put kad se ukaže potreba za njim.

Pošto URL našeg modela zavisi od kolekcije kojoj pripada, moraćemo da obezbedimo nešto što može pružiti to url vlasništvo. Još nismo napisali kolekciju Todos a čak i da jesmo, ne bismo hteli da je koristimo, kao što ne bismo hteli ni da testiramo izolovan Todo model .

Postoji nekoliko načina da se to reši. Za ovaj primer, samo nam je potrebno vlasništvo nad stranim objektom. Najjednostavniji pristup je da ručno lažiramo url vlasništvo. Zatim, jednostavno, možemo pridružiti svoj Todo model našoj lažnoj kolekciji.

Todo.spec.js:

it("should set the URL to the collection URL", function() {
  var collection = {
    url: "/collection"
  };
  this.todo.collection = collection;
  expect(this.todo.url()).toEqual("/collection");
});

Ova specifikacija prolazi bez ikakvog rada s naše strane pošto Backbone.js automatski bira kolekciju za dobavljanje URL-a.

Ovaj pristup je dobar ako vam je lažni objekat jednostavan ili ako ima vrednost vlasništva. Da ste vi lažirani metod i da morate da vidite kako se poziva metod, trebalo bi da razmislite o korišćenju Sinon spy, stub ili mock objekata. Kasnije ćemo doći do toga.

Treba da napišemo drugi primer u slučaju da je podešen id modela. Pomerićemo kolekciju u funkciju beforeEach tako da oba primera mogu da ga koriste bez dupliranja.

Todo.spec.js:

describe("url", function() {
  beforeEach(function() {
    var collection = {
      url: "/collection"
    };
    this.todo.collection = collection;
  });

  describe("when no id is set", function() {
    it("should return the collection URL", function() {
      expect(this.todo.url()).toEqual("/collection");
    });
  });

  describe("when id is set", function() {
    it("should return the collection URL and id", function() {
      this.todo.id = 1;
      expect(this.todo.url()).toEqual("/collection/1");
    });
  });
});

Opet, za prvi put nije potrebno nikakvo kodiranje nove specifikacije. Navedeni primer takođe pokazuje kako se koriste ugnežđeni blokovi za razbijanje specifikacija prema kontekstu. Ovo je uobičajen pristup u ostalim BDD okvirima, kao što je RSpec. Obično biste koristili funkciju beforeEach u svakom kontekstu bloka da biste podesili opisane uslove.

Primer 4: Validacija

Pošto imamo validan URL definisan za model (čak i ako uglavnom potiče od kolekcije) možemo napisati neke provere specifikacije.

Backbone metod validate nije namenjen za direktno pozivanje, već se poziva nakon metoda set() ili save() Indirektno testiranje na ovaj način znači da ne možemo primeniti jednostavan test ulazi-izlaz. Ali, u skladu s BDD tradicijom, testiramo ponašanje našeg koda, a ne njegove unutrašnje funkcije. .

Kad smo implementirali naš Todo izgled objekta, verovatno ćemo poželeti da prikažemo nešto u slučaju da model ne prođe validaciju, da bi korisnik to ispravio. Da bismo to uradili, izgled se vezuje za modelov error događaj i postupa prema tome. Isto možemo uraditi i u svom testu, pa se brinemo za to da se događaj uključi onda kad to očekujemo. Kad povežemo događaj, u povratnom pozivu prosleđujemo funkciju koja treba da se pokrene kad se događaj uključi. Možemo koristiti anonimnu špijunsku funkciju kao povratni poziv:

var eventSpy = sinon.spy();
this.todo.bind("error", eventSpy);

Špijun (spy) će zabeležiti kako se poziva, pa kasnije možemo postaviti svoja očekivanja u vezi s njim u specifikaciji. Hajde da napišemo jednu specifikaciju koja testira da li je model sačuvan i da li se pojavljuje greška kada je naslov prazan. Ne zaboravite da još uvek imamo svoj Todo model kreiran na vrhunskom nivou beforeEach funkcije.

Todo.spec.js:

it("should not save when title is empty", function() {
  var eventSpy = sinon.spy();
  this.todo.bind("error", eventSpy);
  this.todo.save({"title": ""});
  expect(this.eventSpy.calledOnce).toBeTruthy();
  expect(this.eventSpy.calledWith(
    this.todo, 
    "cannot have an empty title"
  )).toBeTruthy();
});

Očekivanja proveravaju da li se povratni poziv špijuna poziva samo jednom. Kada se poziva povratni poziv, poziva se Todo modelom i porukom o neuspehu očekivane validacije. Kad pokrenemo tu specifikaciju, ona se ne pokreće, kao što je bilo očekivano i Jasmine prikazuje sledeću poruku (dva puta):

Expected false to be truthy.

Hmm. Iako su oba neuspeha praćena tragom stoga (stack) i brojevima linije, poruke same po sebi nisu baš od velike pomoći. Uzrok tome je što nema ugrađenih Jasmine tragača za postavljanje očekivanja na Sinon lažnim objektima, pa smo primorani da koristimo toBeTruthy. Srećom, postoji i Jasmine-Sinon plugin koji obezbeđuje ova kustomizovana poklapanja. Kad se uključi u projekat i preusmeri sa jasmine.yml ili SpecRunner.html-a, možete ponovo napisati svoja Sinon očekivanja, poput ovog:

expect(this.eventSpy).toHaveBeenCalledOnce();
expect(this.eventSpy).toHaveBeenCalledWith(
  this.todo,
  "cannot have an empty title"
);

Sada će ta specifikacija prikazati sledeće poruke o neuspehu:

Expected Function to have been called once.
Expected Function to have been called with ...

To je već bolje. Sada treba da napišemo validan metod da bismo omogućili prolaz ovoj specifikaciji.

Todo.js:

var Todo = Backbone.Model.extend({
  ...
  validate: function(attrs) {
    if (!attrs.title) {
      return "cannot have an empty title";
    }
  }
});

Kad ponovo pokrenemo specifikacije, imaćemo zeleno svetlo.

U ovom trenutku, verovatno ćemo poželeti da testiramo da li sačuvani model rezultira očekianim ponašanjem sa našeg Todo modela. Međutim, ostavićemo interakcije servera za kasnije.

Sada ćemo da testiramo kolekcije.

Kolekcije

Za naše Todo aplikacije, treba da kreiramo Backbone.js kolekciju Todo modela. Ova kolekcija objekata biće odgovorna za utovar aktuelnih todo modela sa servera, kao i za standardne liste ponašanja, kao što su naručivanje i filtriranje.

Prvo testirajmo dodavanje modela kolekciji.

Primer 1: Dodavanje modela

Kada dodajete modele kolekciji, Backbone.js automatski će kreirati instance modela tipa koji određuje vaša kolekcija. Na primer:

var Zoo = Backbone.Collection.extend({
  model: Animal
});
var edinburghZoo = new Zoo([
  {name:"Panda"},
  {name:"Penguin"}
]);

Dva slovna objekta, koji su prošli u konstruktor kolekcije, biće korišćeni za kreiranje novog primera modela - Animal .

U našem slučaju, Todos kolekcija obratiće se našem Todo prototipu modela, ali dok pišemo specifikacije jedinice, hoćemo da izolujemo Todos kolekciju i da lažiramo Todo model. Trebalo bi da se pretvaramo da još nismo kreiraliTodo model i da je Todos kolekcija prvo što će, u aplikaciji, biti autorizovano..

U primerima modela, koristili smo jednostavni, ručno napravljeni lažni objekata (stub) u vlasništvu url-a kolekcije jer je samo to bilo potrebno za API kolekciju. U ovom slučaju, kolekcija zapravo instancira novi model, pa moramo da raskrčimo funkciju konstruktora modela. To se postiže kreiranjem Sinon.JS lažnog objekta na sledeći način:

this.todoStub = sinon.stub(window, "Todo");

Ovo izgleda pomalo neobično, ali time jednostavno poručujete: “kreiraj lažni objekat (stub) Todo metoda na prozoru objekta.” Ovo je potpuno validno jer je window.Todo funkcija konstruktora.

Kad god napravimo lažni objekat od permanentnog objekta, treba da ga ponovo povratimo u originalno stanje na kraju svake specifikacije, obično u afterEach funkciji:

this.todoStub.restore();

ili jednostavno:

Todo.restore();

Sledeći deo kreiranja lažnog modela (stub) je da se promeni ono što je konstruktor vratio. Mogli bismo manualno napraviti objekat da izgleda kao Backbone model, ali za to bi nam bilo potrebno mnogo vremena i truda. Umesto toga, koristićemo čisti Backbone.Model konstruktor. To će nam dati pravi backbone model, ali ne i jedan od Todo modela.

beforeEach(function() {
  this.todoStub = sinon.stub(window, "Todo");
  this.model = new Backbone.Model({
    id: 5, 
    title: "Foo"
  });
  this.todoStub.returns(this.model);
});

Mogli bismo očekivati da ćemo time obezbediti da naša kolekcija uvek koristi za primer lažni Todo model. Međutim, referenca za Todo model u kolekciji je već postavljena; u stvari, samo treba da resetujemo model property iz Todos kolekcije pozivanjem:

this.todos.model = Todo

Sada, pošto je naša Todo kolekcija instancirala novi Todo model, uvek će vraćati goli Backbone.js primer modela koji smo kreirali.

Sada možemo napisati svoje specifikacije za dodavanje novih slovnih modela:

Todos.spec.js:

describe("when instantiated with model literal", function() {
  beforeEach(function() {
    this.todoStub = sinon.stub(window, "Todo");
    this.model = new Backbone.Model({
      id: 5, 
      title: "Foo"
    });
    this.todoStub.returns(this.model);
    this.todos = new Todos();
    this.todos.model = Todo; // reset model relationship to use stub
    this.todos.add({
      id: 5, 
      title: "Foo"
    });
  });
    
  afterEach(function() {
    this.todoStub.restore();
  });

  it("should add a model", function() {
    expect(this.todos.length).toEqual(1);
  });
    
  it("should find a model by id", function() {
    expect(this.todos.get(5).get("id")).toEqual(5);
  });
});

Još nismo kreirali svoju Todos kolekciju, a kad pokrenemo te specifikacije, dobijamo sledeću grešku:

ReferenceError: Todos is not defined in .../Todos.spec.js

Zatim kreiramo svoju Todos kolekciju.

Todos.js:

var Todos = Backbone.Collection.extend({
    model: Todo
});

Ponovo imamo zeleno svetlo za pokretanje specifikacije.

Još jednom, dodavanje modela je prilično jednostavno. Možda nećete završiti testirajući sopstvene pakete, ali tehniku pravljenja lažnih Backbone konstrukora objekata moraćete da koristite uvek iznova.

Primer 2: Izdavanje naredbi

Ako obezbedite comparator metod u Backbone.js kolekciji, bilo kom modelu u toj kolekciji naredbe će se izdavati prema nizu ili celom broju koji se vratio iz comparator-a. U našem slučaju, pretpostavimo da hoćemo da izdajemo naredbe prema prioritetu, tako da će na comparator jednostavno vratiti priority atribut, a Backbone.js će uraditi ostalo za nas. .

Možemo napisati specifikaciju za to prilično lako, ali ćemo morati da stvorimo nekoliko modela da bismo ih dodali u kolekciju. Hajde da to uradimo vrhunskim beforeEach metodom, tako da imamo pristup modelima kad nam je to potrebno:

Todos.spec.js:

beforeEach(function() {
  this.todo1 = new Backbone.Model({
    id: 1,
    title: 'Todo 1',
    priority: 3
  });
  this.todo2 = new Backbone.Model({
    id: 2,
    title: 'Todo 2',
    priority: 2
  });
  this.todo3 = new Backbone.Model({
    id: 3,
    title: 'Todo 3',
    priority: 1
  });
});

Hajde da napišemo specifikaciju da bismo testirali svoj metod izdavanja naredbi:

it("should order models by priority", function() {
  this.todos.add([this.todo1, this.todo2, this.todo3]);
  expect(this.todos.at(0)).toBe(this.todo3);
  expect(this.todos.at(1)).toBe(this.todo2);
  expect(this.todos.at(2)).toBe(this.todo1);
});

Kad se pokrene ova specifikacija, Jasmine daje izlazni signal:

should order models by priority by default

praćen dugačkim izlazom očekivanja. Ovo je comparator metod koji dodajemo Todos.js-u da bismo obezbedili prolaz specifikacije.

Todos.js:

var Todos = Backbone.Collection.extend({
  model: Todo,
  comparator: function(todo) {
    return todo.get("priority");
  }
});

Ovaj primer pokazuje da će Backbone kolekcija uzeti bilo koji Backbone model koji obezbedite. Ne mora da bude iste vrste kao što je navedeno u protoptipu kolekcije. Sama kolekcija ne daje primere nikakvih modela jer je snabdevena unapred definisanim modelima. Dakle, još jednom, rad naše specifikacije ne zavisi od Todo modela.

Primer 3: Preuzimanje modela

A sada, pravi izazov: kako ćemo testirati ponašanje jedinice aplikacije kada je u interakciji sa serverom? Ovi testovi često su napisani ili kao funkcionalni testovi koji koriste fiksirane podatke ili kao testovi koji koriste asinhrone jedinice sa pravim odgovorima servera. To je u redu, ali funkcionalni testovi mogu biti spori, teški za postavljanje; zahtevaju od aplikacije da radi na web serveru i zavise od raspoloživosti svih zavisnosti.

Srećom, Sinon.JS zaobilazi ove probleme pružajući mogućnost lažnih odgovora servera. To znači da vaš interakcijski kod složenog, asinhronog servera može biti testiran u brzom okruženju jedinice testiranja. To je blagodet za web programera koji ne može da ima kontrolu nad celim sistemom,. Možete da podesite kako očekujete da server odgovori na vaš zahtev i da testirate kako vaš kod reaguje. Takođe, možete da testirate granične slučajeve, pogrešne odgovore, upravljanje offline i druge ludosti, u vezi s vašom jedinicom. .

Pisaćemo dve specifikacije da bismo testirali da li preuzimanje Todos kolekcije dovodi do kreiranja tačnih Todos modela u kolekciji. Važno je da se testiraju krajnje tačke vaše aplikacije gde dolazi do interakcije s drugim delovima sistema. Ako smo uvereni da strani klijenta ispravno podnosimo zahtev, integracija će biti daleko slobodnija od grešaka i daleko manje problematična.

Pre nego špo napišemo svoje specifikacije, moraćemo da postavimo lažni server. Lažni server, u sušini, je lažni objekat (stub) koji zamenjuje ponašanje pravog servera i ima špijunske osobine da bi zabeležio zahteve koje smo poslali. To radimo u beforeEach funkciji:

beforeEach(function() {
  this.server = sinon.fakeServer.create();
  this.todos = new Todos();
});

Takođe, treba ponovo da uspostavimo normalno funkcionisanje kad pokrenemo svaku specifikaciju u afterEach funkciji:

afterEach(function() {
  this.server.restore();
});

Pošto sad imamo objekat lažnog servera, možemo postaviti očekivanja na osnovu njega i napisati svoju prvu specifikaciju. Time ćemo proveriti da li je zahtev za server tačan. Nećemo tražiti da nam server odgovori - jednostavno ćemo proveriti da li je zahtev primljen.

Todos.spec.js:

it("should make the correct request", function() {
  this.todos.fetch();
  expect(this.server.requests.length)
    .toEqual(1);
  expect(this.server.requests[0].method)
    .toEqual("GET");
  expect(this.server.requests[0].url)
    .toEqual("/todos");
});

Kad pokrenemo ovaj test, Jasmine nalazi Backbone.js grešku:

Error: A 'url' property or function must be specified in ...backbone-min.js

Ovo se lako dâ popraviti, samo dodamo urll svojoj Todos kolekciji.

Todos.js:

var Todos = Backbone.Collection.extend({
  url: "/todos",
  ...
});

Druga specifikacija proverava da li u trenutku, dok server odgovara, kolekcija stvara modele koji predstavljaju JSON koji se vratio. Za to ćemo morati da imamo odgovor svog lažnog servera s nekim JSON podacima. Proširujemo svoju funkciju beforeEach da bi obuhvatila sledeće:

beforeEach(function() {
  this.server = sinon.fakeServer.create();
  this.server.respondWith(
    "GET",
    "/todos",
    [
      200,
      {"Content-Type": "application/json"},
      '{"response":"json response here"}'
    ]
  );
});

Ovde imamo mnogo toga, ali nije tako složeno kao što izgleda. Metod lažnog servera respondWith ovde uzima tri argumenta. Prvi i drugi su HTTP metod zahteva i URL koji odgovara. Poslednji argument je niz koji predstavlja odgovor koji se vratio, a koji i sam ima tri vrednosti: HTTP odgovor koda, slovni objekat sastavljen od zaglavlja odgovora i niza koji sadrži telo odgovora.

U prethodnom primeru napravio sam veoma jednostavan JSON niz odgovora. Međutim, pravi JSON odgovori često imaju duge i složene strukture, a mi zaista ne želimo da oni budu definisani u našim specifikacijama. Iz tog razloga možemo stvoriti fiksture koje se čuvaju u posebnoj datoteci i dele specifikacije po potrebi. Fiksture mogu biti JavaScript izvorni, kreirani, slovni objekti pretvoreni u JSON nizove kad se formuliše odgovor.

Datoteka fikstura može izgledati slično ovoj:

fixtures.js:

beforeEach(function() {
  
  this.fixtures = {
    
    Todos: {
      valid: { // response starts here
        "status": "OK",
        "version": "1.0",
        "response": {
          "todos": [
            {
              "id": 1,
              "title": "Destroy Alderaan"
            },
            {
              "id": 2,
              "title": "Close exhaust port"
            }
          ]
        }
      } 
    }
    
  };
  
});

Koristimo funkciju beforeEach da bi se fiksture ponovo kreirale za svaku specifikaciju (u slučaju da ih beskrupulozni programer modifikuje). Zatim kreiramo grupu Todos fikstura, s jednim “validnim” odgovorom. Na ovaj način možemo da kreiramo slične fiksture za odgovore o greškama i svim ostalim situacijama. Fiksturi se može pristupiti iz specifikacije sa this.fixtures.Todos.valid.

Uz pomoć ugrađenog JSON parsera u modernim pretraživačima i JSON library Doug-a Crockford-a, možemo umetnuti deo koda u starije pretraživače i konvertovati ovu fiksturu u JSON odgovor u respondWith metod:

Todos.spec.js:

beforeEach(function() {
  this.fixture = this.fixtures.Todos.valid;
  this.server = sinon.fakeServer.create();
  this.server.respondWith(
    "GET",
    "/todos",
    [
      200,
      {"Content-Type": "application/json"},
      JSON.stringify(this.fixture)
    ]
  );
});

To još uvek traje malo duže, a ako koristite puno fakeServer odgovora, zauzimaće mnogo prostora. Dok testirate svoju Backbone.js aplikaciju, uglavnom ćete hteti da obezbedite tačan odgovor sa 200 odgovora koda application/json tipom sadržaja i JSON telom. Hajde da napišemo metod koji će nam pomoći da se osećamo ugodnije i koji možemo smestiti u odvojenu datoteku i prilključiti našem paketu specifikacija.

spec-helpers.js:

beforeEach(function() {

  this.validResponse = function(responseText) {
    return [
      200,
      {"Content-Type": "application/json"},
      JSON.stringify(responseText)
    ];
  };

});

Kao i podatke fiksture, metode pomoćnika specifikacija smeštamo u funkciju beforeEach i dodeljujemo metod trenutnom opsegu koji se deli širom svih specifikacija. Naš pozi respondWith sad može da se napiše ponovo:

Todos.spec.js:

this.server.respondWith(
  "GET",
  "/todos",
  this.validResponse(this.fixture)
);

Za potrebe naše specifikacije, moraćemo da otkrijemo koji format će uzeti odgovor. Ako kontrolišete server, onda je lako, možete se pobrinuti za to da odgovor bude jednostavan za rukovanje na klijentu. Međutim, često se dešava da niste odgovorni za odgovor servera, na primer, ako ga programira neki drugi član tima ili ako je izvor eksterni API. U tim slučajevima, često ćete morati da uradite raščlanjivanje nekog JSON odgovora pre nego što se kreiraju modeli.

U našoj gore prikazanoj fiksturi, možete videti da se nizu Todo stavki može pristupiti iz JSON odgovora na response.todos. Da bismo usmerili Backbone.js u pravom smeru, moramo da napišemo parse() na Todos kolekciji. Ova funkcija poziva se kad god se podaci povlače iz servera, kad god se prenosi argument odgovora i mora vratiti niz modela koji predstavljaju kolekciju.

Naša specifikacija za metod raščlanjivanja koristiće fakeServer da odgovori pomenutom JSON fiksturom, a mi ćemo proveriti da li su modeli kreirani u skladu s našim očekivanjima.

Todos.spec.js:

it("should parse todos from the response", function() {
  this.todos.fetch();
  this.server.respond();
  expect(this.todos.length)
    .toEqual(this.fixture.response.todos.length);
  expect(this.todos.get(1).get('title'))
    .toEqual(this.fixture.response.todos[0].title);
});

Mi proveravamo samo jedan model, ali vi ih možete nabrojati preko fikstura i svaki model lako možete podvrgnuti proveri. Unosom this.server.respond() odgovarajućoj tački svoje specifikacije, poručujemo lažnom serveru fakeServer da odgovori našom uspostavljenom fiksturom. Imajte u vidu da ovde nismo morali da napišemo asinhroni test, uprkos činjenici da ovde testiramo sinhrone povratne pozive.

Kad se to pokrene, napušta specifikacije sa sledećom porukom:

Expected 1 to equal 4

Dobijamo samo jedan model jer Backbone pretpostavlja da je objekat vrhunskog nivoa u JSON odgovoru model koji treba da se definiše unutar kolekcije. Hajde da to popravimo:

Todos.js:

var Todos = Backbone.Collection.extend({
  ...
  parse: function(res) {
    return res.response.todos;
  },
  ...
});

Sada kolekcija prima niz slovnih objekata koji liče na modele i specifikacija prolazi.

Isti pristup može se primeniti za lažiranje svih standardnih CRUD operacija koje možete naći u modernim web aplikacijama. Na primer, da biste testirali da li vaša aplikacija ispravno čuva nove modele u serveru, podesili biste fakeServer koji očekuje POST zahteve /todos URL, aa zatim biste pozvali modelov save() metod u specifikaciji.

Rezime

Ovim zaključujemo svoj pogled na testiranje Backbone.js modela i kolekcija. Sledeći put ćemo posebno obratiti pažnju na Backbone.js rutere i, naročito, na poglede koji predstavljaju poseban izazov za testiranje jedinica.

Testranje Backbone aplikacija alatima Jasmine i Sinon – Treći deo. Ruteri i pogledi

Pregled

Ovo je treći i poslednji deo u seriji članaka koji prikazuju kako treba testirati Backbone.js aplikaciju uz pomoć testnog okvira Jasmine BDD i Sinon.JS biblioteke za lažne objekte (spy, stub i mock). Ako još niste pročitali prvi ili drugi deo, pogledajte ih sada.

U ovom poslednjem delu, pogledaćemo neke metode za testiranje Backb one rutera i pogleda. Obe navedene vrste objekata predstavljaju zasebn e izazove za testiranje, ali Jasmine BDD i Sinon.JS obezbeđuju alate k oji su nam potrebni da ih izolujemo i lažiramo eksterni kod i zavisne delove sistema. Proučavaćemo sledeće:

  • različite pristupe testiranju Backbone ruta
  • metode za testiranje prikaza pogleda
  • upotrebu DOM fikstura u vašim specifikacijama
  • upotrebu dodatka jasmine-jquery
  • testiranje pogleda rukovodilaca događaja (event handlers)
  • upotreba lažnih timer-a za manipulisanje vremenskim događajima.

Ruteri

Backbone.jsobjekti rutera zaduženi su za usmeravanje URL hash-eva unutar vaše aplikacije i takođe se mogu koristiti za inicijalizaciju zadataka ako ste tako struktuirali svoj kod.

Kad se ruta nađe u vašoj aplikaciji, Backbone poziva metod rutera udružen s rutom. Takođe, povlači događaj rute u obliku route:[action] gde action predstavlja ime vašeg metoda.

Od vas zavisi da li ćete koristiti metod rutera ili podesiti rukovodioce događajima prema ruti. Imao sam uspeha u korišćenju rukovodilaca događaja za rute. Kasnije možete odrediti ponašanje prema specifičnim objektima u aplikaciji koja treba da odgovori. Metodi pojedinačne metode mogu postati monolitni i teški za testiranje u velikim aplikacijama.

Međutim, za ovaj primer koristićemo metode jednostavne rute. Naš pristup biće da testiramo dva spekta rutera: prvo ćemo testirati rute samih URL-a da se uverimo da će određeni URL pokrenuti određeni metod rute, a zatim ćemo pogledati u metode direktnog testiranja rutera.

Primer 1: Testiranje ruta

Naše Todo aplikacije biće vođene rutama. Kad korisnik prvi put klikne na početnu stranicu, hoćemo da se prikaže njihova Todo lista. U našem kodu, potrebni su sledeći koraci:

  1. Ruter aplikacije odgovara ruti početne stranice (koju predstavlja prazan hash)
  2. Metod home uzima za prime TodoListView i Todos kolekciju (kreiranu u drugom delu ovog članka).
  3. Od Todos kolekcije traži se da izvuče sadržaj iz servera.
  4. Kad se dobije taj odgovor, TodoListView prikazuje listu.
  5. Prikazivanje svake pojedinačne Todo stavke poverava se novim instancama TodoView objekta.

To je mnogo koda za testiranje. Ruter je odgovoran za prva tri koraka. Prvo ćemo pogledati kako bismo testirali da li ruter pravilno reaguje na određenu URL adresu. Ovo bi moglo biti nezgodno jer Backbone.js sistem za rutiranje reaguje na promene u polju adrese pretraživača. Moglo bi se i direktno manipulisati adresom pretraživača, ali Backbone 0,5 (i dalje) obezbeđuje navigate metod na objektima rutera koji se mogu koristiti za simuliranje URL promene.

Obično biste u aplikaciji instancirali ruter po jednom na svakoj strani i pokrenuli Backbone.history.start() da biste započeli slušanje Backbone-ove rute. Međutim, Backbone će vam dozvoliti da pokrenete Backbone.history.start() samo po jednom za svako učitavanje stranice. Ako pokušate da je pokrenete drugi put, dobićete obaveštenje da je došlo do greške.

Najjednostavniji način da to zaobiđete je da umotate poziv kao Backbone.history.start() u try/catch bloku.

Evo specifikacije:

AppRoutes.spec.js:

describe("AppRouter routes", function() {
  beforeEach(function() {
    this.router = new AppRouter;
    this.routeSpy = sinon.spy();
    try {
      Backbone.history.start({silent:true, pushState:true});
    } catch(e) {}
    this.router.navigate("elsewhere");
  });
  
  it("fires the index route with a blank hash", function() {
    this.router.bind("route:index", this.routeSpy);
    this.router.navigate("", true);
    expect(this.routeSpy).toHaveBeenCalledOnce();
    expect(this.routeSpy).toHaveBeenCalledWith();
  });
});

Specifikacija povezuje route:index događaj sa anonimnom funkcijom špijuna Sinon.JS omogućavajući nam da pratimo da li se poziva i na koji način. Zatim osiguravamo da URL fragment ima vrednost koju hoćemo da testiramo; u ovom slučaju, praznu vrednost. Pozivanje funkcije Backbone.history.start() obično bi pokrenulo inicijalnu Backbone.js proveru rutiranja. Međutim, slanjem opcije hash koja obuhvata silent: true izbegavamo momentalno poklapanje rute. Imajte u vidu da po volji možemo koristiti i HTML5 pushState za pretraživače koji ga podržavaju.

Sam primer predstavlja okidač za poklapanje ruta pozivanjem metoda navigate na ruteru sa URL fragmentom kao prvim argumentom. Ako se prenese drugi, istiniti argument, Backbone će takođe pozvati bilo koje metode poklapanja rute i povlačiti rute događaja.

Da bismo obezbedili da metod rute i događaja uvek bude uključen, tokom faze postavke kliknemo negde drugde, samo da bismo se postarali za to da se URL fragmenti razlikuju.

Kad se završi provera rutiranja, očekujemo da naš špijun rute bude pozvan jednom i to bez argumenata, pošto neće biti parametara pridruženih sa početnom rutom.

Kad se pokrene primer, dobijamo očekivanu grešku:

ReferenceError: AppRouter is not defined

Hajde da to popravimo tako što ćemo kreirati svoj AppRouter.Nemojte zaboraviti da ga uključite u jasmine.yml ako je potrebno:

AppRouter.js:

var AppRouter = Backbone.Router.extend();

Ponovno pokretanje specifikacije dovodi do sledeće greške:

TypeError: Cannot call method 'navigate' of undefined

Iz nekog razloga, Backbone.history je nedefinisan, pa na njemu nema navigate metoda. Ispostavlja se da Backbone.js kreira primer Backbone.History (veliko slovo ‘H’) zvani Backbone.history (malo slovo ‘h’) kad se kreira ruter koji ima najmanje jednu određenu rutu. To ima smisla, pošto je upravljanje istorijom potrebno samo kad ima ruta kojima treba odgovoriti.

Sad možemo kreirati svoju rutu:

AppRouter.js:

var AppRouter = Backbone.Router.extend({

  routes: {
    "": "index"
  },
  
  index: function() {}
  
});

i naša specifikacija prolazi.

Pošto je naša ruta indeksa uspešno testirana, hajde da isprobamo detaljnu todo rutu. U jednom trenutku, poželećemo da prikažemo detalje o korisniku za određenu to do stavku. Na primer, mogu se prikazati neke beleške, oznake i informacije o rasporedu. URL fragment za prikazivanje ovog detaljnog pogleda glasio bi: todo/1 za Todo sa idjem 1. Hajde da napišemo specifikaciju da bismo testirali da li naš ruter uspešno upravlja ovim.

AppRoutes.spec.js:

it("fires the todo detail route", function() {
  this.router.bind('route:todo', this.routeSpy);
  this.router.navigate("todo/1", true);
  expect(this.routeSpy).toHaveBeenCalledOnce();
  expect(this.routeSpy).toHaveBeenCalledWith("1");
});

Ova specifikacija vrlo je slična onoj za početnu rutu, ali sada povezujemo špijuna sa route:todo ogađajem i testiramo da li je routeSpy pozivan id parametrom sa URL-a.

Ovo ne uspeva i pojavljuju se sledeće poruke:

Expected Function to have been called once.
Expected Function to have been called with '1'.

Upravo smo to i očekivali. Kreirajmo sada rutu:

AppRouter.js:

var AppRouter = Backbone.Router.extend({

  routes: {
    "": "index",
    "todo/:id": "todo"
  },
  
  index: function() {},
  todo: function(id) {}
  
});

opet, dobijamo zeleno svetlo. Jednostavnim dodavanjem rute hash-u i kreiranjem praznog poziva, obezbeđuje se da route:todo događaj bude otkazan kad se poklopi URL hash.

Mogli bismo da poboljšamo ove specifikacije tako što ćemo za id, staviti samo numeričke vrednosti i mogli bismo proveriti da se naši metodi rute pozivaju umotavajući ih u špijun Sinon.JS-a .

Pošto imamo neke rute, treba da testiramo da li se naši metodi rute ponašaju onako kako bi trebalo.

Primer 2: Testiranje metoda rutera

Kada smo testirali otkazivanje ispravnih ruta, možemo testirati metode rute jednostavno tako što ćemo ih pozvati. Da bismo testirali svoj index metod, moramo se pobrinuti za to da instancira TodoListView i Todos kolekciju na ispravan način. Moraćemo da kreiramo lažne objekte i za jedne i za druge.

AppRouter.spec.js:

describe("AppRouter", function() {

  beforeEach(function() {
    this.router = new AppRouter();
    this.collection = new Backbone.Collection();
    this.todoListViewStub = sinon.stub(window, "TodoListView")
      .returns(new Backbone.View());
    this.todosCollectionStub = sinon.stub(window, "Todos")
      .returns(this.collection);
  });
  
  afterEach(function() {
    window.TodoListView.restore();
    window.Todos.restore();
  });

});

Prvo kreiramo primer rutera za testiranje. Zatim kreiramo goli objekat Backbone.js kolekcija da bi glumio Todos kolekciju koja će se vratiti kad iz nje odstranimo funkciju konstruktora. Na kraju, kreiramo Sinon.JS lažne objekte (stubs) i za TodoListView konstruktora i za konstruktora Todos kolekcije, vraćajući novi Backbone.js View i našu golu kolekciju u skladu s tim.

Sada napišimo specifikaciju:

AppRouter.spec.js:

describe("Index handler", function() {

  describe("when no Todo list exists", function() {
      
    beforeEach(function() {
      this.router.index();
    });
    
    it("creates a Todo list collection", function() {
      expect(this.todosCollectionStub)
        .toHaveBeenCalledOnce();
      expect(this.todosCollectionStub)  
        .toHaveBeenCalledWithExactly();
    });
    
    it("creates a Todo list view", function() {
      expect(this.todoListViewStub)
        .toHaveBeenCalledOnce();
      expect(this.todoListViewStub)
        .toHaveBeenCalledWith({
          collection: this.collection
        });
    });
    
  });
  
});

Pre svake specifikacije, pozivamo index metod za testiranje.

U prvoj specifikaciji, proveravamo da li je konstruktor Todos kolekcije pozvan tačno jednom i to bez argumenata.

U drugoj specifikaciji, proveravamo da li je konstruktor za TodoListView takođe pozvan jednom i to sa hash objektom koji sadrži primer naše lažne kolekcije. Na ovaj način testiramo da li aplikacija povezuje TodoListView s njegovim izvorom podataka, Todos kolekcijom.

Kad se pokrenu te specifikacije, dobijamo četiri neuspela izvršenja:

creates a Todo list collection
  Expected Function to have been called once.
  Expected Function to have been called with exactly.

creates a Todo list view
  Expected Function to have been called once
  Expected Function to have been called with ... 

Napišimo kod da bismo im omogućili prolaz:

AppRouter.js:

var AppRouter = Backbone.Router.extend({
  
  ...
  
  index: function() {
    this.todos = new Todos();
    this.todosView = new TodoListView({
      collection: this.todos
    });
  }
  
});

Jednostavno je. Sada treba da testiramo da li se podaci iz kolekcije preuzimaju kada je index ruta pokrenuta. To se postiže jednostavnim pozivanjem metoda Todos kolekcije: fetch Hajde da napišemo još jednu specifikaciju.

Prvo, treba da lažiramo fetch metod kolekcije tako da ne izvodi nikakvu aktivnost, ali da nam omogući da ga špijuniramo. Dodajemo sledeću liniju našem beforeEach metodu čim kreiramo this.collection:

AppRouter.spec.js:

describe("AppRouter", function() {
    
  beforeEach(function() {
    ...
    this.collection = new Backbone.Collection();
    this.fetchStub = sinon.stub(this.collection, "fetch")
      .returns(null);
    ...
  });
  
  ...
  
});

Zatim možemo dodati svoju novu specifikaciju posle prethodne dve:

it("fetches the Todo list from the server", function() {
  expect(this.fetchStub).toHaveBeenCalledOnce();
  expect(this.fetchStub).toHaveBeenCalledWith();
});

Kao što smo i očekivali, ovo ne uspeva:

fetches the Todo list from the server
  Expected Function to have been called once.
  Expected Function to have been called with.

Jednostavno je napraviti specifikaciju koja će proći:

AppRouter.js:

var AppRouter = Backbone.Router.extend({
  
  ...
  
  index: function() {
    this.todos = new Todos();
    this.todosView = new TodoListView({
      collection: this.todos
    });
    this.todos.fetch();
  }
  
});

Do sada su nam primeri bili jednostavni. Vidite da ruteri lako mogu da kreiraju mnoge objekte, a zatim i metode poziva za te objekte da bi se stvari u vašoj aplikaciji pokrenule.

Ako instancirate svoje inicijalne objekte aplikacija, kao što su ovi u vašim ruterima, onda ćete kreirati mnogo lažnih objekata (stubs i mocks) u svojim specifikacijama rutera. To je stvar koja se tiče programiranja aplikacije. Za jednostavne aplikacije to, verovatno, nije veliki problem, ali ovaj pristup ubrzo postaje glomazan.

Alternativni pristup podrazumeva da instancirate bilo koje inicijalne Backbone.js objekte u ukupnom metodu inicijalizacije aplikacije koji se pokreće kad se stranica prvi put učita, na primer u DOM handler-u. Ruter bi takođe bio instanciran, a Backbone.js history objekat bi se inicijalizovao u ovoj tački. Primarni objekti aplikacije, koje ste kreirali (obično su to pogledi) mogu se spojiti s ugrađenim Backbone.js rutama događaja ili se odvojiti od njih ako se tako nalaže u njihovim kodovima. Ovako uspešno zadužujete pojedinačne objekte aplikacije za određene funkcije, tako da sami odgovaraju za svoju sudbinu. Rezultat toga je kod koji je jednostavan za testiranje i lakši za održavanje. Ako vam specifikacije postanu previše glomazne, dugačke i komplikovane za postavke, to je često znak da kod treba pojednostaviti ili presložiti.

Ako se vratimo unazad i pogledamo početak prvog primera, možemo videti da smo sada testirali prva tri koraka koja su bila neophodna za pravljenje naše to do liste. Poslednja dva koraka pripadaju zaduženjima dva pogleda: TodoListView iTodoView. Hajde da pogledamo kako se testiraju views (Pogledi)

Pogledi

Pošto naša aplikacija koristi jQuery za DOM upravljanje, ima smisla koristiti jQuery za testiranje donetih elemenata koje će proizvesti naši pogledi (views). Srećom, postoji i Jasmine BDD jQuery plugin specijalno za tu svrhu. Dodatak obezbeđuje dve ključne funkcionalnosti: prvo, tu je veliki broj Jasmine matcher-a za testiranje jQuery maskiranih kompleta i elemenata i drugo, pruža mogućnost da kreirate privremene HTML fiksture za upotrebu vaših specifikacija.

Da biste koristili dodatak, samo ubacite jasmine-jquery.js datoteku u svoj jasmine.yml ili SpecRunner.html.

Primer 1: Kreiranje korenskog elementa

U primeru našeg prvog pogleda, kreiraćemo jednostavnu specifikaciju da proverimo da li je naš TodoListView stvorio očekivani element u trenutku inicijalizacije. Backbone.js views kreiraće prazan DOM element čim se inicijalizuje, ali ovaj element neće biti pripojen vidljivom DOM-u. Zahvaljujući tome, view može da se konstruiše bez nepotrebnog uticaja na performanse.

Naša specifikacija prilično je jednostavna:

TodoListView.spec.js:

describe("TodoListView", function() {
  
  beforeEach(function() {
    this.view = new TodoListView();
  });
  
  describe("Instantiation", function() {
    
    it("should create a list element", function() {
      expect(this.view.el.nodeName).toEqual("UL");
    });
    
  });
  
});

Pokretanje ove specifikacije rezultira sledećim neuspehom:

Expected 'DIV' to equal 'UL'.

Ovo lako možemo popraviti u TodoListView.js tako što ćemo odrediti ugrađenu Backbone.js tagName karakteristiku za View:

TodoListView.js:

var TodoListView = Backbone.View.extend({
  tagName: "ul"
});

Proverimo sada da li element ima odgovarajuću klasu:

TodoListView.spec.js:

it("should have a class of 'todos'", function() {
  expect($(this.view.el)).toHaveClass('todos');
});

On koristi toHaveClass matcher kreiran uz pomoć jasmine-jquery dodatka koji deluje u jQuery objektima. Da nismo koristili dodatak, očekivanje bi, otprilike, izgledalo ovako:

expect($(this.view.el).hasClass('todos')).toBeTruthy();

što bi rezultiralo neuspešnim izlazom, poput ovoga:

Expected false to be truthy.

Ovo nam baš i ne pomaže u procesu otklanjanja grešaka. Upotreba jasmine-jquery matcher-a izaziva tu grešku:

Expected '<ul></ul>' to have class 'todos'.

Opet, to možemo lako popraviti jednostavnom karakteristikom className u pogledu objekta.

TodoListView.js:

var TodoListView = Backbone.View.extend({
  tagName: "ul",
  className: "todos"
});

Pređimo, sada, na stvarno testiranje prikaza sadržaja naše to do liste.

Primer 2: Prikazivanje

Kad zatražimo prikaz naše to do liste, ona će kreirati unos zadatka za svaki primer Todo modela u kolekciji Todos. Svaki od ovih zadataka predstavlja primer pogleda s referencom modela koji će biti prikazan.

Dakle, kad se pozove metod TodoListView’s render() hoćemo da testiramo da li je TodoView instanciran za svaki model u pratećoj kolekciji.

Još jednom, pošto trenutno ne testiramo TodoView objekat, odseći ćemo ga koristeći osnovni Backbone.js view. Kao što smo već rekli u drugom delu ovog serijala, mislim da je ovo ubedljivo najlakši način da se Backbone.js objekat izoluje od drugih Backbone.js objekata u vašim specifikacijama bez obraćanja za pomoć lažnim objektima za celokupan Backbone.js interfejs.

Kreiramo osnovni Backbone.js pogled umesto TodoView, a zatim odvajamo funkciju TodoView konstruktora, vraćajući osnovni Backbone.js pogled umesto pravog TodoView primera.

Zatim kreiramo jednostavnu Backbone.js kolekciju sa tri modela i pridružujemo joj TodoList primer. Kad se pozove render() metod pogleda, očekivano ponašanje je da se pozove TodoView konstruktor po jednom za svaki model u kolekciji.

TodoListView.spec.js:

describe("TodoListView", function() {
  
  beforeEach(function() {
    this.view = new TodoListView();
  });
  
  ...

  describe("Rendering", function() {
    
    beforeEach(function() {
      this.todoView = new Backbone.View();
      this.todoViewStub = sinon.stub(window, "TodoView")
        .returns(this.todoView);
      this.todo1 = new Backbone.Model({id:1});
      this.todo2 = new Backbone.Model({id:2});
      this.todo3 = new Backbone.Model({id:3});
      this.view.collection = new Backbone.Collection([
        this.todo1,
        this.todo2,
        this.todo3
      ]);
      this.view.render();
    });
    
    afterEach(function() {
      window.TodoView.restore();
    });
    
    it("should create a Todo view for each todo item", function() {
      expect(this.todoViewStub)
        .toHaveBeenCalledThrice();
      expect(this.todoViewStub)
        .toHaveBeenCalledWith({model:this.todo1});
      expect(this.todoViewStub)
        .toHaveBeenCalledWith({model:this.todo2});
      expect(this.todoViewStub)
        .toHaveBeenCalledWith({model:this.todo3});
    });
    
  });
  
});

Pokretanje ove specifikacije ima za rezultat tri greške:

TypeError: Attempted to wrap undefined property TodoView as function
TypeError: Cannot read property 'calledThrice' of undefined
TypeError: Cannot call method 'restore' of undefined

To nam govori da treba da kreiramo TodoView objekat.

TodoView.js:

var TodoView = Backbone.View.extend();

Kad ponovo pokrenemo specifikacije, pojavljuje se ova greška:

Expected Function to have been called thrice.

a i tri sledeće:

Expected Function to have been called with {..}

Ovo su neuspesi s razlogom. Hajde da ih popravimo tako što ćemo napisati kod koji nam je potreban.

var TodoListView = Backbone.View.extend({

  ...

  render: function() {
    this.collection.each(this.addTodo);
  },
  
  addTodo: function(todo) {
    var view = new TodoView({model: todo});
  }
  
});

Sjajno. Ovo prolazi i sad kreiramo tri TodoViews. Međutim, na stranici se ništa neće prikazati. Moramo da se uverimo da je svaki TodoView’s render() metod u okviru TodoView-a pozvan.

Prvo, treba da špijuniramo lažni TodoView’s render() metod. Podesimo to u beforeEach funkciji.

TodoListView.spec.js:

beforeEach(function() {
  this.todoView = new Backbone.View();
  this.todoRenderSpy = sinon.spy(this.todoView, "render");
  ...
});

a zatim i samu specifikaciju:

it("should render each Todo view", function() {
  expect(this.todoView.render).toHaveBeenCalledThrice();
});

Neuspeh kojim rezultira pokretanje ove specifikacije može se popraviti sledećom promenom linije koja se dodaje render() metodu u TodosView.js:

TodoView.js:

var todoEl = view.render().el;

Međutim, treba još da dodamo prikazani to do svojoj listi. Za tu svrhu koristimo jQuery. Možemo da odstranimo jQuery append metod ili da fizički proverimo da li je taj element dodat. Da bismo napisali specifikaciju za to, prvo treba da kreiramo jednostavan skraćeni render metod na TodoView objektu koji kreira DOM element i vraća se, kao što je:

TodoView.spec.js:

beforeEach(function() {
  this.todoView = new Backbone.View();
  this.todoView.render = function() {
    this.el = document.createElement('li');
    return this;
  };
  this.todoRenderSpy = sinon.spy(this.todoView, "render");
  this.todoViewStub = sinon.stub(window, "TodoView")
    .returns(this.todoView);
  ...
});

i sada možemo napisati specifikaciju da proveri da li je svakom modelu dodat po jedan od ovih elemenata:

it("appends the todo to the todo list", function() {
  expect($(this.view.el).children().length).toEqual(3);
});

To rezultira očekivanim neuspehom:

Expected 0 to equal 3.

Hajde da to popravimo u TodosView.js:

TodosView.js:

addTodo: function(todo) {
  var view = new TodoView({model: todo});
  var todoEl = view.render().el;
  $(this.el).append(todoEl);
}

Pokretanje specifikacije daje istu grešku kao i ranije. Šta se desilo? Ovo je uobičajena začkoljica kada prvi put pokrećete Backbone.js. Pošto se poziva addTodo() metod, kao povratni poziv iz underscore.js, each()iterator, oblast za addTodo nije primer TodoListView već je todo model cilj ciklusa koji se ponavlja. Zbog toga, nema el karakteristika this,i dodavanje ne uspeva.

Srećom, underscore.js obezbeđuje lagodnu funkciju sređivanja oblasti za metodnamed bindAll(). U Backbone.js aplikaciji najbolje je da se poziva u okviru initialize() metoda. Potreban je određeni opseg kao prvi argument i jedan metod ili više metoda za trenutni opseg koji treba da budu podešeni:

initialize: function() {
  _.bindAll(this, "addTodo");
},

Ovako se podešava opseg za addTodo() metod da bi bio TodosView primer, umesto opsega s kojim je, zapravo, bio pozvan.

Sada se jQuery dodatak poziva na tačnom objektu i specifikacija prolazi.

Primer 3: Prikazivanje HTML-a

Do sada, naši pogledi još ništa nisu prikazali. Naš TodoListView jednostavno dodeljuje stvarni prikaz markup-a pojedinačnim TodoView objektima ispod. Testirajmo da li se TodoView elementi prikazuju kao što je očekivano.

Počećemo manipulacijom niza da bismo kreirali HTML markup koji će biti prikazan jQuery’s html() metodom.

U početku ćemo kreirati dve specifikacije. Prva će proveravati da li render() metod vraća primer pogleda. To je neophodno zbog povezivanja i to smo već očekivali kad je u pitanju TodoListView. Druga specifikacija će proveriti da li je dobijeni HTML sasvim u skladu s očekivanjima na osnovu karakteristika primera modela koji je pridodat našem TodoView-u.

Naša beforeEach funkcija za ove specifikacije jednostavno kreira model uzorka, a zatim instancira TodoView i udružuje ga s modelom.

TodoView.spec.js:

describe("TodoView", function() {

  beforeEach(function() {
    this.model = new Backbone.Model({
      id: 1,
      title: "My Todo",
      priority: 2,
      done: false
    });
    this.view = new TodoView({model:this.model});
  });

  describe("Rendering", function() {
    
    it("returns the view object", function() {
      expect(this.view.render()).toEqual(this.view);
    });
    
    it("produces the correct HTML", function() {
      this.view.render();
      expect(this.view.el.innerHTML)
        .toEqual('<a href="#todo/1"><h2>My Todo</h2></a>');
    });
    
  });
  
});

Kad se te specifikacije pokrenu, samo druga postaje neuspela. Prva specifikacija koja testira da li se TodoView primer vraća iz render() metoda, prolazi jer Backbone.js to radi podrazumevano, a mi svojom verzijom još nismo napisali neki drugi render metod.

Druga specifikacija ne uspeva i pojavljuje se sledeća poruka:

Expected '' to equal '<a href="#todo/1"><h2>My Todo</h2></a>'.

Podrazumeva se da render() metod ne kreira markup. Napišimo sada jednostavnu zamenu za render():

TodoView.js:

render: function() {
  var template = '<a href="#todo/{{id}}"><h2>{{title}}</h2></a>';
  var output = template
    .replace("{{id}}", this.model.id)
    .replace("{{title}}", this.model.get('title'));
  $(this.el).html(output);
  return this;
}

To, jednostavno, definiše šablon niza i zamenjuje neka polja označena duplim vitičastim zagradama odgovarajućim vrednostima iz pripojenog modela. Pošto vraćamo TodoView primer iz metoda, prva specifikacija, takođe, prolazi.

Gotovo da nema potrebe ni reći da upotreba HTML niza za testiranje, poput ovoga, izaziva mnoge probleme. Neverovatno je krhka. Ako promenite makar jednu sitnicu iz svog šablona, uključujući prazan prostor, specifikacija neće proći čak i ako prikazani izlaz bude isti. Što vam je šablon složeniji, to će vam biti potrebno više vremena za njegovo održavanje.

Daleko je bolje testirati prikaz izlaza koristeći jQuery za označavanje i proučavanje atributa i vrednosti teksta, elemenata obračuna itd.

Hajde da napišemo specifikacije koje proveravaju neke ključne aspekte očekivanog izlaza. Ponovo koristimo uobičajene matcher-e iz jasmine-jquery dodatka:

TodoView.spec.js:

describe("Template", function() {
  
  beforeEach(function() {
    this.view.render();
  });
  
  it("has the correct URL", function() {
    expect($(this.view.el).find('a'))
      .toHaveAttr('href', '#todo/1');
  });

  it("has the correct title text", function() {
    expect($(this.view.el).find('h2'))
      .toHaveText('My Todo');
  });
  
});

Sada je pravo vreme da pogledamo fiksture elemenata. Do sada smo podešavali jQuery očekivanja prema osobini el pogleda. U mnogim okolnostima, to je sasvim u redu. Zapravo, to se i preporučuje gotovo u svim slučajevima. Međutim, s vremena na vreme, moraćete stvarno da prikažete neku oznak u dokumentu. Najbolji način da se izborite s tim u specifikacijama je da koristite fiksture koje obezbeđuje jasmine-jquery dodatak. Napišimo ponovo tu poslednju specifikaciju tako da koristi fiksture:

TodoView.spec.js:

describe("TodoView", function() {
  
  beforeEach(function() {
 	...
    setFixtures('<ul class="todos"></ul>');
  });
  
  ...
  
  describe("Template", function() {
      
    beforeEach(function() {
      $('.todos').append(this.view.render().el);
    });
      
    it("has the correct URL", function() {
      expect($('.todos').find('a'))
        .toHaveAttr('href', '#todo/1');
    });

    it("has the correct title text", function() {
      expect($('.todos').find('h2'))
        .toHaveText('My Todo');
    });
      
  });
  
});

Sada dodajemo prikazanu todo stavku u fiksturi i podešavamo očekivanja prema fiksturi umesto prema el osobini pogleda. Možda ćete to morati da uradite zbog jednog razloga - kad je Backbone.js view podešen prema prethodnom DOM elementu. Možda bi trebalo da obezbedite fiksturu i da testirate da li el osobina uzima tačan element kad se instancira pogled.

Primer 4: Prikazivanje sa bibliotekom šablona

Sad možemo početi da pravimo složenije šablone tako što ćemo uključiti malo uslovne logike. Kada se todo stavka označi kao izvršena, hoćemo da pružimo korisniku vizuelnu povratnu informaciju u formi drugačije boje pozadine ili, možda, tako što ćemo prikazati precrtan naslov. To ćemo uraditi tako što ćemo klasu dodati sidru.

Napišimo specifikaciju da bismo testirali da li se to dešava.

TodoView.spec.js:

describe("When todo is done", function() {
  
  beforeEach(function() {
    this.model.set({done: true}, {silent: true});
    $('.todos').append(this.view.render().el);
  });
  
  it("has a done class", function() {
    expect($('.todos a:first-child'))
      .toHaveClass("done");
  });
  
});

Kao što smo i očekivali, pokušaj je neuspešan i pojavljuje se sledeća poruka:

Expected '<a href="#todo/1"><h2>My Todo</h2></a>' 
to have class 'done'.

Možemo to popraviti u svom postojećem metodu prikazivanja na sledeći način:

TodoView.js:

render: function() {
  var template = '<a href="#todo/{{id}}">' +
    '<h2>{{title}}</h2></a>';
  var output = template
    .replace("{{id}}", this.model.id)
    .replace("{{title}}", this.model.get('title'));
  $(this.el).html(output);
  if (this.model.get('done')) {
    this.$("a").addClass("done");
  }
  return this;
}

Međutim, videćete da će to ubrzo postati glomazno. Što više logike, to je složenije. Ovde nam može poslužiti biblioteka šablona. Ima mnogo biblioteka, a istraživanje mogućnosti prevazilazi okvire ovog članka. Za ovaj primer koristićemo Handlebars.js.

Moraćemo da dodamo handlebars.js u jasmine.yml ili SpecRunner.html. Trebalo bi da nam je omogućeno ponovno pisanje koda za prikazivanje i da nam postojeće specifikacije prođu bez velikih izmena.

Ovde je naš novi TodoView objekat, modifikovan za handlebars.js:

TodoView.js:

var TodoView = Backbone.View.extend({
  
  tagName: "li",
  
  initialize: function(options) {
    this.template = Handlebars.compile(options.template || "");
  },
  
  render: function() {
    $(this.el).html(this.template(this.model.toJSON()));
    return this;
  }
  
});

Metod initialize sakuplja Handlebars šablon koji je prikazan u vidu niza prilikom instancijacije. Drugi način da se pozovemo na šablon dobija se njegovim smeštanjem na HTML stranicu i dobijanjem istog preko id atributa, koji predstavlja uobičajen pristup kad je u pitanju Handlebars. U realnoj aplikaciji, preporučuje se upotreba prvog pristupa i unos vaših specifikacija u pravi šablon za potrebe testiranja. Jedan od načina da se to uradi, ako vaš projekat koristi Ruby, podrazumeva pomoć Evergreen gem-a koji bi izvršio unos šablona za vas.

Za ono što nama treba, nastavićemo da koristimo pristup ubrizgavanja. Dodajemo novi direktorijum pod nazivom: templates spec a zatim dodajemo novu datoteku pod nazivom todo-template.js koja izgleda ovako:

todo-template.js:

beforeEach(function() {
  this.templates = _.extend(this.templates || {}, {
    todo: '<a href="#todo/{{id}}">' +
            '<h2>{{title}}</h2>' +
          '</a>'
  });
});

Ovako jednostavno kreiramo ili proširujemo objekte šablona u opsegu Jasmine za svaki test i dodajemo scope todo osobinu koja sadrži Handlebars šablon koji hoćemo da koristimo.

Moraćemo da dodamo referencu foldera templates za jasmine.yml ili SpecRunner.html, i da malo ažuriramo postojeće specifikacije da bismo obezbedili šablon kad instanciramo TodoView objekat:

TodoView.spec.js:

describe("TodoView", function() {

  beforeEach(function() {
	...
    this.view = new TodoView({
      model: this.model,
      template: this.templates.todo
    });
  });
  
  ...
  
});

Sve postojeće specifikacije nastavljaju da prolaze s našim novim sistemom šablona na mestu, tako da sada možemo ojačati šablon nekom logikom za izvršeni status:

todo-template.js:

beforeEach(function() {
  this.templates = _.extend(this.templates || {}, {
    todo: '<a href="#todo/{{id}}"{{#if done}} class="done"{{/if}}>' +
            '<h2>{{title}}</h2>' +
          '</a>'
  });
});

Ta specifikacija, takođe, prolazi.

Primer 4: Događaji

Backbone.js pogledi takođe omogućavaju deklaraciju DOM događaja koji treba da se poslušaju i izvrše. API koji to radi je jednostavan: predstavlja hash parove ključ/vrednosti; gde je ključ niz u kome je događaj za koji treba da se veže i selektor koji treba da se koristi, a vrednost označava naziv metoda koji se koristi kao povratni poziv kad se pokrene događaj.

Za našu Todo aplikaciju, obezbedićemo ikonicu za uređivanje za svaku to do stavku koja će zameniti naslov teksta poljem ulaza koje se može uređivati klikom na njega. Napišimo, sada, specifikaciju da bismo proverili to ponašanje:

TodoView.spec.js:

describe("TodoView", function() {

  ...
  
  describe("Edit state", function() {
    
    describe("When edit button handler fired", function() {
      
      beforeEach(function() {
        $('ul.todos').append(this.view.render().el);
        this.li = $('ul.todos li:first');
        this.li.find('a.edit').trigger('click');
      });
      
      it("shows the edit input field", function() {
        expect(this.li.find('input.edit'))
          .toBeVisible();
        expect(this.li.find('h2'))
          .not.toBeVisible();
      });
      
    });
    
  });
  
});

Specifikacija se pokreće i sledeća poruka izveštava nas o neuspehu:

Expected '' to be visible.
Expected '<h2>My Todo</h2>' not to be visible.

Da bismo to popravili, prvo treba da kreiramo link za uređivanje i polje za ulaz u našem šablonu:

todo-template.js:

beforeEach(function() {
  this.templates = _.extend(this.templates || {}, {
    todo: '<a href="#todo/{{id}}"{{#if done}} class="done"{{/if}}>' +
            '<h2>{{title}}</h2>' +
            '<input class="edit" type="text" ' +
            'value="{{title}}" style="display:none"/>' + 
          '</a>' +
          '<a href="#" class="edit">Edit</a>'
  });
});

Zatim dodajemo hash događaja tako što ćemo kliknuti na događaj povezan s event handler-om:

TodoView.js:

var TodoView = Backbone.View.extend({

  ...
  
  initialize: function(options) {
    _.bindAll(this, "edit");
    this.template = Handlebars.compile(options.template || "");
  },
  
  events: {
    "click a.edit": "edit"
  },

  edit: function() {
    this.$('h2').hide();
    this.$('input.edit').show();
  }
  
});

Nemojte zaboraviti da dodate _.bindAll poziv da podesite opseg povratnog poziva uređivanja. Specifikacije su nam opet zelene i možemo da nastavimo.

Primer 5: Animacije i timing

Pretpostavimo da jedan od vaših uvaženih kolega programera smatra da naslov teksta treba da izbledi u roku od pola sekunde kada korisnik klikne na ikonicu “edit”. Naravno, pomislili biste da je to nepotrebno razmetanje u korisničkom interfejsu, ali on je u nadređenom položaju i odmah morate izvršiti instrukciju ili ćete dobiti otkaz.

fadeIn i fadeOut metode:

edit: function() {
  this.$('h2').fadeOut(500);
  this.$('h2').fadeIn(500);
}

Odlično! Sve je dobro dok ne pokrenemo specifikacije i dok nas ne dočeka poruka o neuspehu:

Expected '<h2 style="opacity: 1; ">My Todo</h2>' 
not to be visible.

Ova specifikacija proverava vidljivo stanje naslova odmah posle pozivanja render() metoda. Moramo da sačekamo pola sekunde pre nego što proverimo stanje da bismo animaciji dali dovoljno vremena da se završi.

Jedini način da se to zaobiđe je da se koristi Jasmine-ova ugrađena podrška za asinhrone specifikacije. Postojeća specifikacija mogla bi se ponovo napisati na sledeći način:

TodoView.spec.js:

describe("When edit button handler fired", function() {
  
  beforeEach(function() {
    $('ul.todos').append(this.view.render().el);
    this.li = $('ul.todos li:first');
    this.li.find('a.edit').trigger('click');
  });
  
  it("shows the edit input field", function() {
    waits(510);
    runs(function() {
      expect(this.li.find('input.edit'))
        .toBeVisible();
      expect(this.li.find('h2'))
        .not.toBeVisible();          
    })
  });
  
});

S ovim pristupom, čekamo 510 millisekunde između klika događaja i očekivanja koji su umotani u runs() poziv koji treba da ih pokrene posle završenog čekanja.

To i nije tako loše, a specifikacija sada prolazi. Međutim, napišite više od nekoliko asinhrono tempiranih specifikacija i dobićete veoma spor prikaz paketa specifikacija. Naš paket specifikacija je od 0.15 sekundi došao do 0.65 sekundi samo zbog jedne specifikacije.

Da bismo eliminisali to prolongiranje, koristimo mogućnostiSinon.JS lažnog timing-a. Umesto da čekamo pola sekunde, Sinon.JS omogućava vam da lažiramo prolazak vremena. Nažalost, tako ne upravljamo stvarno continuum-om prostor/vreme, što bi predstavljalo veoma uredno programiranje, već jednostavno vara prirodno vreme JavaScript-a tako što zadržava metode poput setTimeout i setInterval.

Možemo ponovo da napišemo našu specifikaciju tako da koristi Sinon.JS lažne timer-e na sledeći način:

TodoView.spec.js:

describe("When edit button handler fired", function() {
  
  beforeEach(function() {
    this.clock = sinon.useFakeTimers();
    $('ul.todos').append(this.view.render().el);
    this.li = $('ul.todos li:first');
    this.li.find('a.edit').trigger('click');
  });
  
  afterEach(function() {
    this.clock.restore();
  });
  
  it("shows the edit input field", function() {
    this.clock.tick(600);
    expect(this.li.find('input.edit'))
      .toBeVisible();
    expect(this.li.find('h2'))
      .not.toBeVisible();
  });
  
});

Lažni timer-i se inicijalizuju pozivanjem sinon.useFakeTimers() u beforeEach metodu. Moramo vratiti funkcije prirodnog timer-a u njihovo originalno stanje posle naših specifikacija i zato kreiramo afterEach funkciju koja to radi. Konačno, sama specifikacija odgovorna je za pomeranje sata unapred za određeni broj milisekundi pre nego što pokrenemo svoja očekivanja.

Sada, pošto naše specifikacije prolaze, ponovo se pojavljuju na oko 0,15 sekundi. Iako naša aplikacija sada koristi animacije, one su imale minimalan efekat na naše specifikacije. To je, svakako, dobro jer daje dizajnerima i programerima fleksibilnost da podese karakteristike interfejsa, kao što su animacije bez neopravdanog zadržavanja teksta.

Upotreba lažnih timer-a nije ograničena samo na animacije, naravno. Oni se mogu koristiti gde god je timing važan u vašoj aplikaciji. Na primer, možete imati redovan zahtev da ažurirate neke informacije na svaki minut. Umesto da pustite specifikaciju da traje čitav minut, ili da veštački menjate interval u specifikaciji, možete ubrzati timer i testirati drugi zahtev koji ste uputili serveru .

Sažeti pregled

Testiranje ponašanja i interakcija korisničkog interfejsa mogu ponekad biti zastrašujuće, a testovi se često usporavaju ili ostanu nedovršeni zbog jedinstvenih izazova interfejsa prisutnih web aplikacija. Iako su neke od ovde opisanih tehnika specifične za Backbone.js, mnoge se odnose na jQuery i druge aplikacije bogate web interfejsima uopšte.

Kroz ovu seriju članaka koncentrisali smo se na pisanje jedinica testova gde su pojedini JavaScript objekti testirani u izolaciji. Vaš paket testova treba da uključi i neke testove integracije gde su objekti testirani u kombinaciji s drugim objektima, kao i funkcionalne testove gde se proverava stvarno pokretanje aplikacija pomoću automatskog pretraživača diver-a, kao što su Selenium ili Web Driver. Postoji prilično veliki broj okvira, biblioteka i driver-a koji zadovoljavaju ovu potrebu, ali oni se teško postavljaju i podložni su greškama koje se teško otklanjaju. Iz tog razloga, od suštinskog je značaja da paket testova otkrije što više problema za što kraće vreme i da se piše o greškama otkrivenim prilikom testiranja. .

Nadam se da vam je ova serija članaka dala neke korisne tehnike da započnete testiranje svojih Backbone.js aplikacija, a ne da se obeshrabrite zbog očigledne složenosti koja može da vam upadne u oči na prvi pogled. Poput bilo kog naizgled složenog zadatka, testiranje podrazumeva jednostavno razbijanje zadatka na manje jedinice koje se lakše koriste i upotrebu kompleta alata koji ovaj proces čine bržim i efikasnijim. Srećno testiranje!

Published (Last edited): 27-02-2013 , source: http://tinnedfruit.com/2011/03/03/testing-backbone-apps-with-jasmine-sinon.html