Před časem jste se na našem blogu mohli dočíst, jak používáme Redux. V tomto příspěvku se posuneme o něco dál a podíváme se na to, jak děláme integrační testování Reduxu.

Náš kód se snažíme pokrývat unit testy. Redux nám to v mnohém usnadňuje, protože všechny jeho komponenty jsou funkce bez vedlejších účinků. Zjistili jsme ale, že v některých ohledech nám unit testy nevyhovují a proto jsme začali uvažovat o testech integračních.

Jak vypadá náš kód? Náš projekt se skládá z více modulů a v rámci každého z nich máme generátory akcí, reducer a selektory (více viz Redux definuje tolik potřebná omezení na velkém projektu). Každou z těchto komponent můžeme testovat. Tyto testy jsou však problematické z několika důvodů (příklady v angličtině):

  1. Jsou často triviální. Generátory akcí jsou například velmi primitivní, téměř zde není co testovat.
  2. Jsou fragilní – příliš těsně svázané se strukturou stavu. Například testy reducerů fungují tak, že si připravím nějaký stav, aplikuji na něj akci a zkontroluji výsledný stav. Pokud však změním strukturu stavu, například kvůli optimalizaci, musím všechny testy reducerů přepsat.
  3. Netestují kontrakt mezi komponentami. Pokud přepíšu reducer, čímž změním strukturu stavu, ale zapomenu upravit selektory, testy mi to neodhalí.

Testování modulů

Tyto problémy nás vedly k myšlence testovat celý modul – kombinaci akcí, reduceru a selektorů. Naše testy zkoušíme strukturovat následujícím způsobem:

  1. Vycházíme z iniciálního stavu (ten získáme tak, že reducer aplikujeme na nedefinovaný stav a prázdnou akci).
  2. Na iniciální stav pomocí reduceru aplikujeme posloupnost akcí (tak vytvoříme generátory).
  3. Zkontrolujeme, že selektory vrací očekávanou hodnotu.

import {combineReducers} from 'redux-immutable';
import {setEntity, clear} from './actions';
import {NAME} from './constants';
import reducer from './reducer';
import {getEntity} from './selectors';
  
describe('Entity module', () => {
    const wrappedReducer = combineReducers({[NAME]: reducer}); // nutné, protože selektory předpokládají, že se stav nachází pod klíčem NAME
    const initial = wrappedReducer(undefined, {});
    it('clear action clears all single entities', () => {
        let state = wrappedReducer(initial, setEntity(JEDI, anakin));
        state = wrappedReducer(state, setEntity(SITH, vader));
        state = wrappedReducer(state, clear());
        expect(getEntity(state, JEDI, anakin.key)).to.not.exist();
        expect(getEntity(state, SITH, vader.key)).to.not.exist();
    });
});

Máme dokonce mockovaný store, který to vše provádí, ale jeho kód je vcelku primitivní. Tím, že pracujeme pouze s rozhraním modulu – akcemi a selektory – a tím, že začínáme z počátečního stavu, se zbavujeme závislosti na tom, jak konkrétně stav vypadá a můžeme jej volně refaktorovat. Zároveň nám testy zaručí, že chování modulu zůstane i po refaktoru stejné.

Drobným problémem jsou složitější testy, kde vytvoření počátečního stavu může znamenat poměrně dlouhou posloupnost akcí. V tomto případě by se daly použít jako počáteční stav výsledné stavy předchozích testů. Rozhodně ale nechceme počáteční stav deklarovat přímo. Samozřejmě nepočítáme s asynchronními akcemi (komunikace se serverem).

Závěrem

Tento způsob testování reduxových aplikací je u nás relativně nový a teprve jej zavádíme. Myslíme si však, že úvaha za nimi je správná. Řešíme ještě například jejich jmenné konvence. Vzhledem ke struktuře (počáteční stav, akce, ověření) se zdá nejvhodnější syntaxe given-when-then známá z behavior-driven development.


Rád bys pracoval na zajímavých projektech s nejlepšími technologiemi?

Hledáme do naší party skvělé vývojáře! Aktuálně hledáme do Brna a do Hradce Králové Java/JEE Developera i Senior Java/JEE Developera.

Případně mrkni na všechny naše otevřené pozice a dej nám o sobě vědět, bez ohledu na to, zda máš rád front-end nebo back-end, jestli nějakou technologii umíš nebo ne. Pokud budeš ty sám chtít, dostaneš u nás příležitost naučit se, co tě zajímá nebo udělat něco výjimečného.