Záludnosti testování softwaru

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.

Jak těžké je psaní testů?

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ží. 

  • Stačí vám, že máte pokrytou velkou část kódu a u každé pipeliny se vám zobrazí zelený check a tím máte splněno, nebo chcete reálné otestování kódu? 
  • Chcete, aby test opravdu zachytil chybu, kterou novým kódem způsobíte, nebo aby se naopak rozbíjel, když upravíte jen implementaci? 
  • Máte testy rozdělené do logických celků, nebo jeden velký integrační test, který se táhne jako jQuery špageta, a vy stihnete 2 kafe, než projde?
  • Máte strukturovanou codebase, nebo je splácaná v pár souborech? 

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.

Testování softwaru v praxi: nástroje jsou základ

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.

Jak mockovat různé věci a nezbláznit se z toho?

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.

Mockování side-effect modulů

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:

-- CODE language-js -- // logger export const log = (message: string) => { console.log(message) } // logger.test describe("logger", () => { beforeEach(() => { jest.resetModules() // mock all modules again jest.doMock("./") }) test("runs side-effect code", async () => { await import("./logger") // side-effect code runs here }) test("runs side-effect code again", async () => { await import("./logger") // side-effect code runs here }) })

Mockování side-effect modulů: proměnná se inicializuje mimo funkci

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.

-- CODE language-js -- // logger const randomControlNumber = Math.random() export const shouldLog = () => { return randomControlNumber < 0.1 } // logger.test describe("logger", () => { test("mocks math", async () => { const spyOnMathRandom = jest.spyOn(global.Math, "random").mockReturnValue(0.1) const { shouldLog } = await import("./logger") expect(shouldLog()).toBe(0.1) }) })

Potřebuju mock, ale jenom tak napůl

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: 

-- CODE language-js -- const DataContext = React.createContext(null) export const DataProvider = ({ children }: Props) => { //... return ( {children} ) } export const useData = () => { return React.useContext(DataContext) }

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.

-- CODE language-js -- jest.mock("../", () => { const originalModule = jest.requireActual("../") return { ...originalModule, useData: jest.fn(), } })

Mockování asynchronního kódu

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ů.

Pár užitečných nástrojů pro úspěšnější testování SW

1. isolateModules a isolateModulesAsync

  • Pokud potřebujete testovat vícekrát naimportovaný modul, mohou se hodit tyto funkce. Ty vám v testu vytvoří sandbox prostředí a zajistí, aby se jednotlivé importy mezi sebou nepraly.

2. Testování několika scénářů

  • Relativně nedávno jsem objevil each funkcionalitu, díky níž můžete daný test nebo celou sadu provést pro několik vstupů najednou.

3. Rozšíření match funkcí

  • Zajímavým rozšířením Jestu je knihovna jest-extended. Ta nabízí několik funkcí, které využijete na denní bázi při psaní testů a která vám ušetří pár úhozů na klávesnici.

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!

Záludnosti testování softwaru
Ondřej Hajný
Frontend Developer
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.