Jednoduché testování – Jest

jestPln 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í

testing pyramid
Testing pyramid

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

jest-output
Jest output

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.

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *