Jednoduché testování – Jest
Pln nových poznatků z dalšího ročníku konference AgentConf18 z Rakouského Dornbirnu jsem se rozhodl vyzkoušet pro psaní testů na novém projektu nový testovací nástroj od Facebooku – Jest. Jedná se komplexní testovací nástroj, který zvládá více než jen unit testy a moje zkušenosti s tímto nástrojem jsou vesměs pozitivní. Rád bych Vám v následujícím článku řekl i ukázal, co Jest umí a v čem je lepší než mnou donedávna používaný Mocha.
Instalace
V první řadě, instalace. Jak je u Nodejs zvykem, nejedná se o nic zázračného, naopak. Stačí využít balíkovacího nástroje a stáhnout si Jest a přidat si ho jako závislost (dependency) k Vašemu projektu. Pokud neděláte žádné šílenosti, nebudete asi potřebovat mít Jest v produkčním bundlu a tedy, stejně jako já si ho přidejte pouze pro devové prostředí. Osobně používám YARN, asi ten Facebook prostě žeru.
yarn add --dev jest
Pokud lintujete, budete ještě potřebovat následující modul, jinak se vám v IDE a testech rozsvítí kód jak vánoční stromeček.
yarn add --dev eslint eslint-plugin-jest
Integrace
Tenhle blok bude stručný, protože k integraci Jestu do Vaší aplikace není vlastně třeba nic víc dělat. Přidejte si pouze run script do package.json, který spustí testy a máte hotovo. Jest je plný magie a zázraků a všechny potřebné require a importy si sesmolí na pozadí.
"scripts": { "test": "jest" }
Použití

Jest nám pomůže hlavně s psaním Unit testů. Unit testy jsou ty nejdůležitější, jejich napsání bývá zpravidla nejméně náročné a proto by je měla mít každá aplikace. Odhalení chyb v počátcích životního cyklu aplikace, tedy při vývoji je mnohem levnější než na produkci, po nasazení. O testech více například zde. Jest nám ale umí pomoci i testováním komponent.
Psaní testů je celkem hračka. Není třeba žádných dalších importů a další programátorské byrokracie. Prostě vytvořte soubor s koncovkou .test a můžete začít psát testy.
Pro psaní testů můžete využít následující integrované funkce. Některé jsem přezval z oficiální dokumentace a připojil vlastní zkušenost a některé jsem napsal sám.
Common matchers
Základní testy pro porovnávání, které umí pracovat i s porovnáváním objektů. Oproti Mocha není nutno používat deep equal pro porovnávání složitějších objektů.
test('two plus two is four', () => { expect(2 + 2).toBe(4) })
test('object assignment', () => { const data = {one: 1} data['two'] = 2 expect(data).toEqual({one: 1, two: 2}) })
Arrays
Test na existence prvku v poli.
const shoppingList = [ 'diapers', 'kleenex', 'trash bags', 'paper towels', 'beer', ] test('the shopping list has beer on it', () => { expect(shoppingList).toContain('beer') })
Numbers
Porovnávání čísel umožňuje i různé zajímavé funkce typu větší, větší nebo rovno apod. Dále je možno porovnávat i čísla s plovoucí řádovou čárkou, pomocí closeTo, nebo-li blížící se…
test('two plus two', () => { const value = 2 + 2 expect(value).toBeGreaterThan(3) expect(value).toBeGreaterThanOrEqual(3.5) expect(value).toBeLessThan(5) expect(value).toBeLessThanOrEqual(4.5) // toBe and toEqual are equivalent for numbers expect(value).toBe(4) expect(value).toEqual(4) }) test('adding floating point numbers', () => { const value = 0.1 + 0.2 expect(value).toBe(0.3) // This won't work because of rounding error expect(value).toBeCloseTo(0.3) // This works. })
Strings
Porovnávání stringů (textů) je velmi zajímavé, není třeba totiž definovat, jak vlastně chcete string porovnat. Prostě použijete match a můžete použít i regexp na složitější konstrukce. Pokud máte s psaním regexpu problém, zkuste si nechat pomoci od některého z online nástrojů jako je například zde.
test('there is no I in team', () => { expect('team').not.toMatch(/I/) }) test('but there is a "stop" in Christoph', () => { expect('Christoph').toMatch(/stop/) })
Exceptions
Testování vyjímek je velmi podobné jako v Mocha, nicméně zápis je o něco jednoduší. Můžete testovat pouze to, že je vyhozena vyjímka, stejně tak i její typ, její znění a můžete použít i regexp na text vyjímky.
function compileAndroidCode() { throw new Error('you are using the wrong JDK') } test('compiling android goes as expected', () => { expect(compileAndroidCode).toThrow() expect(compileAndroidCode).toThrow(Error) expect(compileAndroidCode).toThrow('you are using the wrong JDK') expect(compileAndroidCode).toThrow(/JDK/) })
Truthiness
Null vs undefined vs false vs true. V javascriptu velké téma, které trochu nabourává snahu vyhnout se mixed datovým typům. Pro testování těchto hodnot lze použít následující výrazy.
test('null', () => { const n = null expect(n).toBeNull() expect(n).toBeDefined() expect(n).not.toBeUndefined() expect(n).not.toBeTruthy() expect(n).toBeFalsy() }) test('zero', () => { const z = 0 expect(z).not.toBeNull() expect(z).toBeDefined() expect(z).not.toBeUndefined() expect(z).not.toBeTruthy() expect(z).toBeFalsy() })
Callbacks
Když už vás někdo vydírá a musíte callback použít, dá se také jednoduše testovat. Je třeba dát pozor na zavolání done po vykonání testu, jinak se test neukončí a neprojde.
function fetchData(callback) { callback } // Don't do this! test('the data is peanut butter', () => { function callback(data) { expect(data).toBe('peanut butter') } fetchData(callback); }) test('the data is peanut butter', done => { function callback(data) { expect(data).toBe('peanut butter') done() } fetchData(callback); })
Promises
Testování promises není žádná věda. Zajímavou přidanou hodnotou je nutnost definovat počet assercí, aby se zkontrolovalo, že byly veškeré testy vyhodnoceny.
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('peanut butter') }, 2000) }) } test('the data is peanut butter', () => { expect.assertions(1) return fetchData().then(data => { expect(data).toBe('peanut butter') }) })
Async await
Pro študované je zde i možnost testovat budoucí náhradu promises, která se konečně oficiálně dostala do standardu ES2017.
function fetchData() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('peanut butter') }, 2000) }) } test('the data is peanut butter', async () => { expect.assertions(1) const data = await fetchData() expect(data).toBe('peanut butter') })
Mocks
Používání mocků je dobrým pomocníkem, pokud chcete testovat pouze konkrétní kusy kódu. Lze si s jejich pomocí například nasimulovat výsledky vnitřně volaných funkcí.
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } } test('the data is peanut butter', () => { const mockCallback = jest.fn() forEach([0, 1], mockCallback) // The mock function is called twice expect(mockCallback.mock.calls.length).toBe(2) // The first argument of the first call to the function was 0 expect(mockCallback.mock.calls[0][0]).toBe(0) // The first argument of the second call to the function was 1 expect(mockCallback.mock.calls[1][0]).toBe(1) })
DOM testing
S Jestem není ani testování DOMu žádný problém. Tedy se můžeme posunout v testovací pyramidě o úroveň výše. K testování využívá enzyme, nicméně nejedná se o end-to-end testy a tedy testování neprobíhá v reálném prohlížeči, jako je to například v Seleniu. Můžete si ale zkontrolovat, že se Vaše komponenta vykreslí správně a že na kliky a další uživatelské akce reagují Vaše komponenty jak mají.
import React from 'react' import { configure, shallow } from 'enzyme' import Adapter from 'enzyme-adapter-react-16' import Countdown from '../../src/components/countdown' configure({ adapter: new Adapter() }) test('CheckboxWithLabel changes the text after click', () => { const countdown = shallow(<Countdown />) expect(countdown.find('button').text()).toEqual('Vypnout') }) test('CheckboxWithLabel changes the text after click', () => { const countdown = shallow(<Countdown />) countdown.find('button').simulate('click') expect(countdown.find('button').text()).toEqual('Zapnout') })
Setup and Teardown
Stejně jako v Mocha i zde není žádný problém nastavit si před/po spuštěním každého testu různé akce a volání.
beforeAll(() => console.log('1 - beforeAll')) afterAll(() => console.log('1 - afterAll')) beforeEach(() => console.log('1 - beforeEach')) afterEach(() => console.log('1 - afterEach')) test('nothing', () => { })
Snapshots
Zajímavou funkcionalitou je možnost snapshot testingu. Při spuštění testu dojde k vygenerování HTML otisku komponenty, lze však samozřejmě i vložit vlastní kód. Při dalším testu je kontrolováno, že uložené HTML je stejné jako nově vygenerované. Tato funkcionalita se velmi hodí například když přepisujete starší projekt do Reactu a výsledkem má být stejné HTML.
import React from 'react' import Countdown from '../../src/components/countdown' import renderer from 'react-test-renderer' it('renders correctly', () => { const tree = renderer .create(<Countdown/>) .toJSON() expect(tree).toMatchSnapshot() })
Jest components
Zajímavostí je, že jednotlivé součásti Jestu jsou použitelné v rámci Vašeho projektu i samostatně. Můžete například použít modul pro kontrolu podobnosti dvou výstupů se zobrazením rozdílů – jest-diff.
const diff = require('jest-diff'); const a = {a: {b: {c: 5}}}; const b = {a: {b: {c: 6}}}; const result = diff(a, b); // print diff console.log(result);
Závěr

Závěrem bych chtěl dodat, že Jest na mne při používání působil velmi svižně. Testy probíhají asynchronně s průběžným výpisem výsledků i shrnutím a velmi detailním přehledným výstupem. Minimálně se při jejich běhu nenudíte a stále máte na očích výsledek, který se drží na konci. Při jeho používání jsem se nemohl zbavit pocitu, že byl vytvořen s cílem nahradit ostatní současné testovací nástroje, což se mu dost možná podaří. Toto tvrzení podporuje i fakt, že s vydáním Jestu je k dispozici i nástroj pro převod testů napsaných v jiných nástrojích, např Mocha.