JavaScript na serveru: Testování a kontinuální integrace

node.js logo

Dnešní díl je věnován testování. Tématem budou jednotkové a integrační testy a kontinuální integrace pomocí Travis CI. A pokud máte účet na GitHubu, tak se dnes také můžete poprvé přímo zapojit do vývoje naší aplikace.

Seriál: Node.js - s JavaScriptem na serveru (13 dílů)

  1. JavaScript na serveru: Začínáme s Node.js 23.11.2010
  2. JavaScript na serveru: Patří budoucnost Node.js? 21.9.2012
  3. JavaScript na serveru: Architektura a první Hello World 5.10.2012
  4. JavaScript na serveru: moduly a npm 12.10.2012
  5. JavaScript na serveru: začínáme programovat e-shop 19.10.2012
  6. JavaScript na serveru: MongoDB, Mongoose a AngularJS 26.10.2012
  7. JavaScript na serveru: Testování a kontinuální integrace 2.11.2012
  8. JavaScript na serveru: REST API 9.11.2012
  9. JavaScript na serveru: implementace REST API 16.11.2012
  10. JavaScript na serveru: nástroje a dokumentace 23.11.2012
  11. Začínáme s AngularJS 30.11.2012
  12. AngularJS direktivy a testování 7.12.2012
  13. JavaScript na serveru: CoffeeScript a šablonovací systémy 14.12.2012

Souběžně se seriálem o Node.js vychází na Zdrojáku i seriál o testování od Josefa Zamrzly. Ačkoliv je zaměřen na PHP, teorie platí i pro ostatní platformy, Node.js nevyjímaje. 

Pokud ještě nejste přesvědčeni, že se automatizované testování vyplatí, pak si přečtěte článek o tom, co se může stát, jsou-li automatizované testy z procesu vývoje aplikace vynechány. 

Podobně jako v minulých dílech si můžete prohlédnout všechny zdrojové kódy tohoto dílu v repozitáři na GitHubu. Také je k dispozici demo aktuální verze aplikace.

Jednotkové a integrační testy

V průběhu seriálu se budeme používat především dva druhy testů: jednotkové (unit) a integrační.

Unit testy se používají pro testování izolované části kódu, nejčastěji metody třídy. Závislosti na ostatní objekty jsou nahrazeny falešnými objekty, které požadované chování simulují. Jednotkové testy jsou díky tomu rychlé a podporují tvorbu kvalitního kódu.

Integrační testy prověřují, zda jednotlivé části systému spolu správně fungují. Na rozdíl od unit testů mohou používat externí zdroje (např. databáze), díky čemuž je však jejich běh pomalejší. 

Oba druhy testů jsou stejně důležité a neměly by při nasazení na produkci chybět. Jestliže v aplikaci chybí jednotkové testy, pak bude nejspíš aplikace otestována jen pro nejzákladnější scénáře. Chybí-li v aplikaci integrační testy, nikdy nemůžeme říct, že spolu jednotlivé části systému komunikují správně (např. že vytvořený dotaz na databázi skutečně vrací to, co očekáváme).

Testovací nástroje

V Node.js lze najít velké množství různých testovacích nástrojů. Pro jednotkové testování jsou nejpopulárnější frameworky Mocha, Jasmine nebo Vows. Pro integrační testování aplikací nad Expressem je výborný modul supertest. Pro srozumitelné asertace se hodí modul should.js. Pro mocky či stuby se hodí balíček SinonJS. Pro mockování modulů je výborný modul horaa. Přehled dalších zajímavých modulů pro testování nabízí projekt jster.net.

Pro náš e-shop budeme používat moduly mocha, supertest, should.js a horaa. Až se budeme věnovat klientskému frameworku AngularJS, tak si ještě představíme test runner Testacular.

Nastavení prostředí

Nastavení celého testovacího prostředí pro Node.js je velmi jednoduché a skládá se z několika kroků:

a) úprava souboru package.json

Zde je potřeba upravit sekci scripts a přidat informaci, jakým způsobem se mají testy spouštět. V případě balíčku mocha je to jednoduché, stačí zadat pouze příkaz mocha. V takovém případě bude testy framework hledat v adresáři /test. Nyní můžeme spouštět testy přes nástroj npm zadáním příkazu npm test. Toto nastavení je důležité např. pro Travis CI, který tímto způsobem spouští automatizované testy.

Druhá změna se týká přidání sekce devDependencies, která má stejnou strukturu jako sekce dependencies, pouze s tím rozdílem, že zde se zadávají moduly, které souvisí přímo s vývojem a na produkční server se neinstalují.

Přidané klíče a hodnoty v package.json vypadají takto:

  "scripts": {
    "test": "mocha"
  },
  "devDependencies": {
    "mocha": "1.6.x",
    "supertest": "0.4.x",
    "should": "1.2.x",
    "horaa": "0.1.x"
  }

b) úprava konfigurace 

Pro integrační testy bude ještě potřeba nastavit konfiguraci pro testovací prostředí, takže v souboru config.js vedle sekce production a developement přidáme do konfigurace ještě sekci test pro inicializaci spojení s databází, která bude sloužit jen pro integrační testy. Zároveň rozdělíme soubor server.js na dva soubory app.js a server.js  – také kvůli integračním testům, jak si ukážeme dále v článku.

app.configure('test', function(){
     app.set('db uri', 'mongodb://localhost/zdrojaktest');
});

c) nastavení modulu mocha

Nakonec je potřeba ještě říct frameworku mocha, jaké nastavení pro testování chceme použít. Mocha hledá při spuštění soubor mocha.opts v adresáři /test. Zde se píšou stejné parametry, které lze zadat i v příkazovém řádku. Naše nastavení vypadá takto:

--require ./test/mocha.js
--require should
--reporter dot
--ui bdd
--recursive

První příkaz říká, že máme načíst před testy soubor mocha.js, který nastavuje prostředí na „test“. Dále načítáme modul should.js. Jako reporter chceme použít dot, který je ze všech nejjednodušší. Přehled všech reportérů je v dokumentaci. Styl testování bude BDD (behaviour driven development). Poslední parametr recursive říká, že chceme projít všechny adresáře v adresáři test rekurzivně (jinak by mocha spouštěl testy pouze v daném adresáři).

Testování s frameworkem Mocha

V adresáři test vytvoříme další dva adresáře: unit a api. Do adresáře unit budeme vkládat jednotkové testy a obsah adresáře bude kopírovat strukturu aplikace. Do adresáře api budeme vkládat integrační testy, které budou testovat API jednotlivých zdrojů.

Jak vypadá struktura testů? Podívejme se na nejjednodušší test, který testuje funkci pro převod názvu na URL.

var url = require(process.cwd() + '/lib/filters/url');

describe('url filter', function(){
    it('prevede mezery na pomlcky', function(){
        url('nejaky nazev stranky').should.eql('nejaky-nazev-stranky');
    })
});

Používají se dvě funkce: describe() a it(). Funkce describe() sdružuje související testy, it() pak obsahuje samotný test. Odkud se vzal objekt should? Ten ke všem objektům přidává should.js jen pro účely testování. Na objektu should pak můžeme volat mnoho různých metod pro asertace. Jejich kompletní přehled je v dokumentaci. Metoda eql() porovnává dva různé výsledky. Vše je navrženo tak, aby testy byly co nejlépe čitelné, takže předchozí test lze přeložit takto:

Filter url() převede mezery na pomlčky. Výsledek zavolání funkce url() s parametrem ‚nějaký název stránky‘ by se měl rovnat řetězci ‚nejaky-nazev-stranky‘.

Jak vidíte, testy mohou také sloužit jako užitečný a vždy aktuální doplněk dokumentace systému.

Pull Request

Jistě jste si všimli, že funkce url() neřeší mnoho dalších případů. Určitě bychom chtěli z řetězce odstranit diakritiku a zvláštní znaky, také se obvykle URL převádí na malá písmena. To už je ale úkol pro vás :-) Můžete si sami vyzkoušet přidávat další testy a chcete-li, můžete se přímo zapojit do tvorby našeho e-shopu tím, že si e-shop “forknete” v GitHubu, přidáte test a odešlete Pull Request. Jak to přesně udělat, najdete ve screencastu o GitHubu

Zjednodušeně je potřeba:

  1. Vytvořit fork repozitáře Zdroják (odkaz “Fork” vpravo nahoře).
  2. Stáhnout si nový repozitář k sobě přes git clone https://github.com/[vaše GitHub username]/Zdrojak.git.
  3. Upravit kód funkce url() a přidat test.
  4. Spustit ostatní testy a ujistit se, že vše funguje ( npm test).
  5. Odeslat změny na GitHub do forknutého repozitáře ( git commitgit push).
  6. Kliknout na odkaz Pull Request a popsat, co se změnilo.

Pokud si to nejprve chcete vyzkoušet nanečisto, pak má GitHub pro tento účel testovací repozitář Spoon Knife.

Další unit testy

Kromě testů pro filter url() přibyly ještě testy pro middleware fields a model Page.

Middleware fields zkoumá, zda se v URL nenachází parametr fields obsahující seznam polí, které se mají v rámci dokumetu vrátit. Určitě jste si minule všimli, že v controlleru Page se vrací v akci  index() všechna pole, ačkoliv vypisujeme jen názvy a URL. Pokud budeme přidávat do URL parametr fields, tak říkáme, že chceme vracet jen určitá pole z dokumentu (a _id, které vracíme vždy).

Testy pro middleware fields mohou vypadat takto:

var fields = require(process.cwd() + '/app/middleware/fields')();

describe('middleware fields', function(){
   it('rozdeli parametr fields na samostatne polozky', function(){
       var req = { query: { fields: 'field1,field2,field3' } }
       fields(req, {}, function() {});
       req.zdrojak.fields.should.eql({
           field1: 1, field2: 1, field3: 1
       });
   })

   it('odstrani z vracenych polozek prazdne polozky', function(){
       var req = { query: { fields: 'field1, ,field2,field3,' } }
       fields(req, {}, function() {});
       req.zdrojak.fields.should.eql({
           field1: 1, field2: 1, field3: 1
       });
   })
});

Další ukázka testů se týká modelu Page, kde testujeme metodu findOneByUrl(), která jen zjednodušuje volání metody  findOne().

Schema.statics.findOneByUrl = function(url, cb) {
   Model.findOne({url: url}, cb);
};

Tohle je již obtížnější případ, protože se volá metoda jiné třídy, a jak bylo řečeno v úvodu, při jednotkovém testování se testuje pouze jeden objekt a všechny ostatní závislosti jsou nahrazeny falešnými objekty. K tomu se výborně hodí balíček horaa, který metodou hijack() vytvoří falešnou implementaci metody findOne() a po skončení testu zase metoda restore() vrátí původní implementaci zpět.

var PageHoraa = require('horaa')(process.cwd() + '/app/models/Page');
var Page = require(process.cwd() + '/app/models/Page');

describe('model Page', function(){
   describe('metoda findOneByUrl()', function() {
       it('zavola findOne() s podminkou pro vyber dokumentu podle url', function(){
           PageHoraa.hijack('findOne', function(cond) {
               cond.should.eql({url: 'url-stranky'})
           });
           Page.findOneByUrl('url-stranky', function(){});
           PageHoraa.restore('findOne');
       })
   });
});

Do projektu ještě přibyl middleware error, který se však v příštím díle bude výrazně měnit a proto ho zde popisovat nebudu.

Integrační testy

Integrační testy se tvoří podobně jako klasické unit testy. Navíc se však používá balíček supertest, který se používá pro simulaci zpracování HTTP požadavku. Integrační test pro získání všech stránek přes API pages může vypadat takto:

var request = require('supertest');
var app = require(process.cwd() + '/app');

describe('API pages', function(){
    describe('GET /api/pages', function(){
        it('vrati seznam vsech polozek v databazi', function(done){
            request(app)
                .get('/api/pages')
                .expect(200)
                .end(function(err, res) {
                    res.body.length.should.eql(2);
                    res.body[0].should.include({url:’abc’});
                    done();
                });
        });
    });
});

Funkci request() předáme jako parametr konfiguraci naší aplikace v Expressu a dále zavoláme metodu get(), která simuluje vyslání HTTP GET požadavku na URL /api/pages. Metoda expect() ověří, že aplikace vrátila HTTP kód 200. V metodě end() ověřujeme, že požadavek vrátí pole ve formátu JSON, které bude mít dva prvky, a že první prvek bude mimo jiné obsahovat i klíč url s hodnotou “abc”. 

Protože se při testu dotazujeme i na testovací databázi, vytváříme si testovací data, která před každým testem vložíme do databáze. Předtím však samozřejmě všechny dokumenty z kolekce odstraníme. K tomu slouží funkce beforeEach(), která proběhne před každým testem. 

Zajímavý může být také test vložení nového dokumentu:

describe('POST /api/pages', function(){
    it('vlozi novou stranku do databaze', function(done){
        request(app)
            .post('/api/pages')
            .send({title: 'titulek ABC', content: 'lorem ipsum set dolorem'})
            .expect(200)
            .end(function(err, res){
                Page.findOne({title: 'titulek ABC'}, function(err, doc) {
                    doc.title.should.equal('titulek ABC');
                    doc.content.should.equal('lorem ipsum set dolorem');
                    done();
                });
           });
     });
});

Zde navíc ověřujeme, že byl skutečně nový dokument vložen do databáze. 

Spuštění všech testů v prostředí Windows může vypadat takto:

Ještě se zpětně vrátím k odkazovanému příběhu ze začátku článku. Co myslíte, kdybychom stejným způsobem použili tyto testy na objednávky, nastala by stejná situace, kdy e-shop nefungoval, ačkoliv při ručním testování žádný problém nebyl? Velmi pravděpodobně ne. Minimálně integrační test podobně jako u testování vkládání přes POST /api/pages by selhal, protože by se nová objednávka do databáze nedostala. Abychom se ale ještě pojistili proti případu, že zapomeneme testy spustit, je vhodné používat nástroje pro kontinuální integraci.

Kontinuální integrace

Travis CI je platforma pro kontinuální integraci. Lze zde snadno propojit účet na GitHubu s účtem na Travis CI. Ten po každém commitu do sdíleného repozitáře spustí testy a zkontroluje, zda testy v pořádku prošly. Je možné nastavit, že v případě selhání některého z testů přijde určené osobě mail.

Travis CI používá velké množství známých firem pro své open source projekty, např. Yahoo! nebo Mozilla. Travis CI není jen pro Node.js, podporuje také PHP, Javu, Python i Ruby. Kromě toho je možné spouštět i testy nad databází MongoDB, MySQL, PostgreSQL, CouchDB, Redis, ElasticSearch a mnoho dalších.

Můžete si prohlédnout informace o průběhu testů u našeho e-shopu. Zde je podrobně vidět, jak docházelo k instalaci všech balíčků pro test. Na konci obsahu záložky Current je pak zeleně podbarven výsledek testování. V záložce Build History si můžete projít zpětně několik posledních buildů.

Ikonka o průběhu testování se pak často vkládá do souboru README.md, takže je hned při návštěvě repozitáře vidět, zda testy projektu procházejí.

Napojení na Travis CI

Napojení je velice jednoduché. Do rootu projektu stačí přidat soubor .travis.yml s tímto obsahem:

language: node_js
node_js:
  - 0.8
services:
  - mongodb

Znamená to, že chceme projekt testovat nad Node.js 0.8.x a jako databázi použijeme MongoDB. Dále je potřeba se přihlásit přes GitHub kliknitím na ikonku Přihlášení přes GitHub. Travis CI si nahraje informace o projektech, které na GitHubu máte a pak je potřeba daný projekt v administraci Travis povolit a kliknout na Sync now. A to je vše. Od té chvíle se už o všechno ostatní Travis CI bude starat sám, a to včetně vytvoření testovací databáze.

Travis CI není jediný nástroj pro kontinuální integraci, velmi populární je také např. Jenkins CI.

Co dále

Příští díl bude věnován API. Bude obsahovat informace o tom, jak co nejlépe navrhnout API webové aplikace podle best practices, a o tom, jak může při tvorbě API Node.js pomoci.

Na tvorbě tohoto článku se svými připomínkami podílel také Pavel Lang (github). Díky!

Jakub pracoval na několika zajímavých projektech, za nejvýznamnější považuje vytvoření e-commerce řešení Shopio.cz. Poslední rok se plně věnuje Node.js, frameworku AngularJS a NoSQL databázím.

Komentáře: 6

Přehled komentářů

langpa Nodeunit
Čelo kafíčko
Jakub Mrozek Re: kafíčko
Clary Re: kafíčko
Segeda Re: kafíčko
langpa Re: kafíčko
Zdroj: http://www.zdrojak.cz/?p=3728