Testovací trio Mocha, Chai a Sinon

Publikováno: 14.2.2018

Představíme vám testovací framework Mocha, knihovny Chai a Sinon. Ukážeme, k čemu slouží, a na co si dát pozor při jejich používání. Dále vysvětlíme, jak se pracuje s knihovnou Sinon, která slouží pro mockování/stubování a vytváření špiónů.

Celý článek

Mocha

Jedná se o javascriptový testovací framework (test runner), který běží na NodeJS. Jednoduše řečeno sériově spouští testy a vytváří reporty.

Mocha vs Jasmine

Jasmine je také jeden z oblíbených testovacích frameworků. Osobně používám spíše Mocha, protože s ním pracuji už nějaký ten pátek. Navíc se mi nelíbí, jak se v Jasmine přeskakují a spouští konkrétní testy. Mocha intuitivně spouští konkrétní testy pomocí metody only a přeskakuje je pomocí metody skip. V Jasmine se musí dát před konkrétní testovací případy písmeno „x“ a písmeno „s“. Dodnes nemám tušení co znamená „x“ a co „s“.

Chai

Chai je BDD/TDD assertovací knihovna. Chai nám poskytuje “syntaxi”, pomocí které zapisujeme testy. Podporuje několik stylů zápisu testů:

  1. should – foo.should.be.a('string');
  2. expect – expect(foo).to.be.a('string');
  3. assert – assert.typeOf(foo, 'string');

Vyberte si styl, který vám nejvíce vyhovuje. Osobně jsem si zvykl používat styl expect.

Sinon

Jedná se o knihovnu, díky které můžeme vytvářet stuby, mocky a spies. Pomocí stubů a mocků se dá testovat pouze konkrétní větev funkce. Díky spies (špiónům) můžeme zjistit, kolikrát se konkrétní metoda zavolala a jaké parametry měla na vstupu.

Jak to slepit všechno dohromady

K instalaci všech knihoven použijeme balíčkovací systém npm. Stačí zadat příkaz npm install sinon mocha chai sinon-chai chai-as-promised. Tímto příkazem si nainstalujeme všechny knihovny, které budeme potřebovat. Knihovna sinon-chai doplní podporu sinonu do chai a bude nám sloužit pro assertování mocků/stubů/špiónů. Chai-as-promised poskytne lepší práci s asynchronním kódem.

Hlavička testovacího souboru bude vypadat následovně.

const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const sinonChai = require('sinon-chai');
const chaiAsPromised = require('chai-as-promised');

chai.use(chaiAsPromised);
chai.use(sinonChai);

Popis syntaxe

Sekce describe popisuje problém. Většinou se začíná názvem třídy a pokračuje se popisováním konkrétní situace, kterou chceme testovat. Describe můžeme zanořovat jak se nám zlíbí, např.

describe('Some Class', () => {

   describe('when user logged in', () => {

       it('should do some stuff', () => {

       });

   });

   describe('when user logged out', () => {

       it('should do some boring stuff', () => {

       });

   });

});

V Sekci it pak testujeme konkrétní testovací případ pomocí expect/assert/should syntaxe z knihovny chai, např.:

it('should properly sum two numbers',() => {
    expect(sum(4, 4)).to.equal(8);
});

Ještě existují tzv. “setup hooky”, které se spouští před “it” nebo před “describe”:

  1. beforeEach, afterEach – spouští se před/po každým “it/describe”
  2. before, after – spouští se jen 1x před “it/describe”

Začínáme testovat

Synchronní testy

Napišme si jednoduchou funkci pro sčítání dvou čísel.

function sum(a, b) {
   return a + b;
}

Jednoduchý synchronní test, který bude testovat, že součet čísel 4 a 4 je 8, může vypadat následovně.

describe('Some Math Class', () => {
  
   it('should properly sum two numbers', () => {
       expect(sum(4, 4)).to.equal(8);
   });

});

Očekáváme, že výstup funkce sum se bude rovnat 8.

Asynchronní testy

Vytvořme si funkci fetchInvoices, který nám bude vracet Promise s daty (fakturami) A, B, C.

function fetchInvoices() {
   return Promise.resolve(['A', 'B', 'C']);
}

Otestujme, že nám funkce vrací správné faktury.

it('should fetch correct invoices', async () => {
   expect(await fetchInvoices()).to.deep.equal(['A', 'B', 'C']);
});

Pomocí async/await syntaxe je asynchronní testování snadné. My si na ty faktury prostě počkáme a až dojdou, tak otestujeme, že nám prvky (faktury) v poli sedí. Pozor, musíme použít deep equal. Chceme zkontrolovat opravdu jednotlivé prvky v poli a ne referenci.

Testování vyjímek

Synchronní

Chceme otestovat, že nám funkce divide vyhodí vyjímku, když budeme dělit nulou. Když jsem začínal s testy, tak jsem to intuitivně napsal následovně:

it('should throw division error if divide by zero', () => {
   expect(divide(4, 0)).to.throw('Division by zero!');
});

Pokud spustíme testy, tak uvidíme chybovou hlášku typu: Error: Division by zero! Problém je v tom, že se vyhodí vyjímka ještě před tím, než se dostane do testovacího frameworku. Musíme tedy volání divide v expectu obalit do anonymní funkce:

it('should throw division error if divide by zero', () => { 
   expect(() => divide(4, 0)).to.throw('Division by zero!'); 
});

Asynchronní

Definujme si asynchronní funkci registerUser, která vrací Promise a vyhodí vyjímku „User already exists“.

function registerUser(username) {
   return new Promise((resolve, reject) => {
      throw new Error('User already exists!');
   });
}

Test bude vypadat následovně:

it('should throw error if user already exists', async () => {
  await expect(registerUser('Milos Zeman')).to.be.rejectedWith('User already exists!');
});

Očekáváme, že promise vrácená z metody registerUser bude zamítnuta (rejectnuta) s chybou „User already exists“.

Stubování a špionáž

Pro stubování a špionáž použijeme knihovnu Sinon. Ta má spoustu zajímavých metod. Nás momentálně zajímají dvě. Metoda spy a metoda stub. Metoda stub (v překladu pahýl) je jednoduchá funkce s předprogramovaným chováním. Nejlépe ji pochopíme na příkladu z oficiální dokumentace.

const callback = sinon.stub();
callback.withArgs(42).returns(1);
callback.withArgs(1).throws("TypeError");

callback(); // Nic nevrací, žádná vyjímka
callback(42); // Vrátí 1
callback(1); // Vyhodí TypeError

Spy se používá, když potřebujeme zkontrolovat, kolikrát se konkrétní metoda zavolala a jaké měla parametry.

const callback = sinon.spy();
callback();

expect(callback).to.have.been.calledOnce;

Očekáváme, že funkce callback byla jednou zavolána.

Příklad

Definujme funkci manageDateForCouple.

function manageDateForCouple(boy, girl) {
   if (girl.isPretty()) {
       boy.takeGirlToExpensiveRestaurant(girl);
   } else {
       boy.buyGirlKebab(girl);
   }
}

Chceme zařídit rande pro kluka a slečnu. Pokud je slečna hezká, tak ji kluk vezme do nějaké drahé restaurace. Když není, tak jí koupí jen kebab.

Náš první test bude testovat „když je slečná hezká tak se zavolá metoda takeGirlToExpensiveRestaurant„. K testování potřebujeme aby metoda isPretty na objektu girl vracela true (stub) a potom je potřeba ověřit, že se zavolala metoda takeGirlToExpensiveRestaurant na objektu boy (spy). Vytvoříme si tedy stub na metodě isPretty, kterí vrací true. Potom vytvoříme spy na metodě takeGirlToExpensiveRestaurant.

describe('when managing date for couple', () => {

   describe('and girl is hot', () => {

       describe('boy', () => {

           it('should take her to expensive restaurant', () => {
               const hotGirl = {
                   isPretty: sinon.stub().returns(true)
               };
               const boy = {
                   takeGirlToExpensiveRestaurant: sinon.spy(),
               };

               manageDateForCouple(boy, hotGirl);

               expect(boy.takeGirlToExpensiveRestaurant).to.have.been.calledOnce;
           });

       });

   });

});

Poté otestujeme, že kluk koupí slečně kebab v případě, že hezká není. Vytvoříme si stub na metodě isPretty, kterí vrací false. Potom vytvoříme spy na metodě buyGirlKebab.

describe('and girl is ugly', () => {

   describe('boy', () => {

       it('should buy her kebab', () => {
           const hotGirl = {
               isPretty: sinon.stub().returns(false)
           };
           const boy = {
               buyGirlKebab: sinon.spy(),
           };

           manageDateForCouple(boy, hotGirl);

           expect(boy.buyGirlKebab).to.have.been.calledOnce;
       });

   });

});

Pár řádku nakonec

Doporučuju projít si dokumentaci Sinonu, protože obsahuje celou škálu metod, které se určitě budou hodit. Dále pěkný článek od Fowlera „Mocks are not stubs“, kde vysvětluje rozdíl mezi mockem a stubem.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace