Testování kódu je dnes samozřejmostí u většiny softwarových projektů. Alespoň doufám, že je, a pokud ne, tak by určitě mělo. Ostatně už jsme to rozebírali v článku Jak být nejhorším programátorem. Dnes bych se rád podíval na některé záludnosti, na které jsme narazili při testování frontendové aplikace pro Kiwi.
Na tuto větu bych odpověděl šalamounsky: „To záleží…“ Koneckonců je to kód a ten nemusí být složitý, ale může. Na čem tedy závisí, jak složité bude napsat testy?
Určitě na tom, jak moc vám na testech záleží.
Důvodů, proč by mohlo být psaní testů náročné, nebo dokonce otravné, může být několik. Budeme však počítat s tím, že víte, proč kód testujete, máte testy rozumně strukturované, berete jejich psaní jako součást programování a jde vám především o to, aby testy pomáhaly zachytit nežádoucí chyby a tím zrychlit vývoj a usnadnit práci QA testerům. 🙂
S tímto předpokladem se vrhneme rovnou na pár konkrétních situací, do kterých vás může testování nejen frontendových aplikací dostat.
V rámci projektu v Kiwi, na kterém pracujeme, používáme pro testování standardní nástroje, které najdete u většiny projektů s Reactem: Jest a Testing Library. Jest je základ, který má sice své mouchy, ale je léty prověřený a s Reactem funguje dobře. O to lépe, pokud se spojí s Testing Library, bez které si dnes testování nedokážu představit.
Nejenže Testing Library poskytuje spoustu drobných utilit pro usnadnění častých akcí, ale hlavně tlačí programátora do psaní testů, které povedou k tomu, aby testoval aplikaci tak, jak ji bude používat uživatel. To znamená testovat funkcionalitu a ne implementaci. Může se to zdát jako jasná věc, ale jako vývojáři máme často tendenci na aplikaci pohlížet našima očima a ne očima uživatele, pro kterého je aplikace určená.
Takové testy ale nemusí být zrovna šťastné řešení – proč tomu tak je, si můžete i s příklady přečíst v článku Testing Implementation Details od autora knihovny.
Při psaní integračních a unit testů celkem rychle narazíte na potřebu nahradit danou implementaci jinou tak, aby váš test opravdu testoval jen daný soubor nebo funkci. Čím obsáhlejší část vaší aplikace test zahrnuje, tím více mocků pravděpodobně budete potřebovat. V následujících odstavcích se podíváme na situace, kdy se mockování trochu komplikuje.
Když potřebujete mockovat standardní exportovanou funkcionalitu, využijete jednoduše <inline-code>`jest.mock(‘your_module’)`<inline-code>. Co když ale potřebujete otestovat modul, který začne něco dělat hned při importu?
Na takovou situaci jsem narazil a její řešení je víc než jednoduché: prostě daný modul naimportujete až v místě, kde ho testujete pomocí asynchronního importu <inline-code>`await import(‘your_module’)`<inline-code>.
Pro jeden test scénář to funguje, ale co když chci v dalším testu vyzkoušet jinou variantu? Nastane problém, protože daný modul už je jednou naimportovaný, a tedy uložený v module cache. Proto se při druhém importu side-effect kód nespustí.
Tento problém lze vyřešit použitím funkce <inline-code>jest.resetModules()<inline-code> v <inline-code>beforeEach()<inline-code> callbacku. V tom případě ale musí programátor zajistit opětovné namockování a hlavně naimportování všech modulů. Jak totiž název funkce napovídá, opravdu se vyresetují všechny moduly použité v souboru. Přibyde tak několik řádek kódu, které se musí znovu vykonat:
Podobně na tom budeme, pokud se například nějaká proměnná inicializuje mimo funkci. V příkladu níže vidíme zadefinování náhodného čísla do proměnné, která se použije ve funkci později. V tomto případě klasický mock opět nebude fungovat, protože samotná proměnná se inicializuje už při importu, kdežto mockování probíhá až potom.
Situaci ale můžeme zase vyřešit použitím <inline-code>`await import`<inline-code>, čímž inicializaci proměnné oddálíme až po namockování <inline-code>Math.random<inline-code> funkce.
Pokud kód nerozdělujete striktně do jednotlivých souborů, můžete snadno narazit na situaci, kdy potřebujete namockovat jen část modulu (jednu funkci, třídu apod.). Představte si následující soubor:
Jednoduchý provider, který používá hook pro zabalení kontextu. V praxi pak daný provider použijete v nadřazené nebo přímo v root komponentě a samotný hook v komponentě níže.
Všechno funguje, jak má, kód máte pěkně u sebe a samostatný kontext, provider i hook se dají otestovat jednoduše. Problém nastává, když potřebujete otestovat komponentu, u které potřebujete namockovat hook, ale ponechat implementaci samotného provideru. To může nastat, třeba pokud daný provider používáte při setupu testů v custom rendereru.
Tady se hodí funkce <inline-code>jest.requireActual()<inline-code>, která vrací původní implementaci modulu. Hodí se i v jiných případech – pokud například máte soubor exportující více funkcí (helpers, utils apod.) a chcete namockovat pouze některou z nich.
Dříve či později určitě narazíte na potřebu zahrnout v kódu mock asynchronní funkce. Na to má Jest pěkné API, jen někdy nemusí fungovat tak, jak byste si mysleli. Na problém jsem narazil při testování side effect importů.
Chtěl jsem nasimulovat první zavolání úspěšné a druhé neúspěšné. Kód, který by měl fungovat, jsem napsal následovně:
<inline-code>jest.fn().mockResolvedValueOnce({your: ‘value’}).mockRejectedValue<inline-code>
Bohužel v takové kombinaci mock úplně nefunguje a já byl nucen tyto 2 varianty rozdělit do dvou samostatných testů.
1. isolateModules a isolateModulesAsync
2. Testování několika scénářů
3. Rozšíření match funkcí
Podívali jsme se na jednu kategorii testování, která vám může způsobit komplikace. Doufám, že aspoň některé tipy využijete při testování svých aplikací a tím snížíte počet bugů v produkci. Přeji vám spoustu chycených chyb a oprávněných zelených fajfek v pipeline!