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ě):
- Jsou často triviální. Generátory akcí jsou například velmi primitivní, téměř zde není co testovat.
- 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.
- 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:
- Vycházíme z iniciálního stavu (ten získáme tak, že reducer aplikujeme na nedefinovaný stav a prázdnou akci).
- Na iniciální stav pomocí reduceru aplikujeme posloupnost akcí (tak vytvoříme generátory).
- 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.
Napsat komentář