Testování kódu je dnes standardní praxí ve většině softwarových projektů. Aspoň doufám, že je, a pokud ne, rozhodně by být mělo. Věnoval jsem se tomu již v článku ‘Jak být nejhorším programátorem.’ Dnes bych se rád zaměřil na záludnosti, na které jsme narazili při testování frontendové aplikace pro Kiwi.
Jak těžké je psaní testů?
Na tuto otázku bych odpověděl diplomaticky: ’Záleží…” Koneckonců je to kód, a kód nemusí být složitý, ale může být. Takže na čem závisí obtížnost psaní testů?
- Stačí vám vysoké pokrytí kódu a zelený checkmark v každém pipeline, abyste to považovali za hotové, nebo chcete skutečné testování kódu?
- Chcete, aby testy skutečně zachytily chyby zavedené novým kódem, nebo by se měly rozbít, když změníte jen implementaci?
- Jsou vaše testy organizovány do logických jednotek, nebo máte jeden masivní integrační test, který běží jako jQuery špagety, zatímco dopijete dvě kávy?
- Máte strukturovanou kódovou základnu, nebo je rozptýlena po několika souborech?
Může být několik důvodů, proč může být psaní testů obtížné nebo dokonce frustrující. Předpokládejme však, že chápete, proč kód testujete, vaše testy jsou přiměřeně strukturovány, psaní testů považujete za součást programování a vaším hlavním cílem je pomoci zachytit nechtěné chyby, urychlit vývoj a usnadnit práci QA.
Testování softwaru v praxi: nástroje jsou základ
Pro projekt Kiwi, na kterém pracujeme, používáme standardní nástroje, které najdete ve většině React projektů: Jest a Testing Library. Jest je základ — má svá specifika, ale je prověřený a funguje dobře s Reactem. Ještě lépe v kombinaci s Testing Library, bez které si testování nedokážu představit.
Testing Library nejenže poskytuje četné utility zjednodušující běžné akce, ale hlavně tlačí vývojáře k psaní testů, které vedou k testování aplikací tak, jak je budou používat uživatelé. To znamená testování funkcionality, ne implementace. Může se to zdát zřejmé, ale jako vývojáři máme tendenci nahlížet na aplikace vlastní optikou, nikoli z pohledu uživatele.
Jak mockovat různé věci a nezbláznit se z toho?
Při psaní integračních a unit testů budete brzy potřebovat nahradit implementaci jinou, aby váš test skutečně testoval pouze daný soubor nebo funkci. Čím širší pokrytí váš test má, tím více mocků pravděpodobně budete potřebovat.
Mockování side-effect modulů
Když potřebujete mockovat standardní exportovanou funkcionalitu, jednoduše použijete jest.mock(‘your_module’). Co ale, když potřebujete testovat modul, který při importu začne něco dělat? Řešení je jednoduché: importujte modul asynchronně pomocí await import(‘your_module’), kde ho testujete.
To funguje pro jeden testovací scénář, ale co když chcete vyzkoušet další variantu v dalším testu? Problém nastane, protože modul je již jednou importován a uložen v cache modulů. Side-effect kód se při druhém importu nespustí. To lze vyřešit pomocí jest.resetModules() v beforeEach() callbacku — programátor však musí zajistit opětovné mockování a reimportování všech modulů.
// logger
export const log = (message: string) => {
console.log(message)
}
// logger.test
describe("logger", () => {
beforeEach(() => {
jest.resetModules()
// znovu namockujte všechny moduly
jest.doMock("./")
})
test("spustí side-effect kód", async () => {
await import("./logger")
// side-effect kód se spustí zde
})
test("spustí side-effect kód znovu", async () => {
await import("./logger")
// side-effect kód se spustí zde
})
})
Mockování side-effect modulů: proměnná se inicializuje mimo funkci
Podobné výzvy nastávají, pokud se například proměnná inicializuje mimo funkci. V příkladu níže vidíme náhodné číslo definované v proměnné, která se používá později ve funkci. V tomto případě klasické mockování nefunguje, protože proměnná se inicializuje při importu, zatímco mockování se stane poté. Toto lze vyřešit pomocí await import, který opozdí inicializaci proměnné až po mockování Math.random.
// logger
const randomControlNumber = Math.random()
export const shouldLog = () => {
return randomControlNumber < 0.1
}
// logger.test
describe("logger", () => {
test("mockuje 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 striktně neoddělujete kód do jednotlivých souborů, snadno narazíte na situace, kdy potřebujete mockovat pouze část modulu. Představte si soubor s context providerem, který exportuje jak DataProvider, tak hook useData. Problémy nastávají, když potřebujete mockovat hook při zachování implementace provideru — například pokud používáte provider při nastavení testu ve vlastním rendereru.
const DataContext = React.createContext(null)
export const DataProvider = ({ children }: Props) => {
//...
return (
<DataContext.Provider value={...}>
{children}
</DataContext.Provider>
)
}
export const useData = () => {
return React.useContext(DataContext)
}
Zde se hodí funkce jest.requireActual(), která vrátí původní implementaci modulu. Je užitečná i v jiných případech — pokud máte soubor exportující více funkcí a chcete mockovat pouze některé:
jest.mock("../", () => {
const originalModule = jest.requireActual("../")
return {
...originalModule,
useData: jest.fn(),
}
})
Mockování asynchronního kódu
Dříve nebo později určitě narazíte na potřebu zahrnout do kódu mock async funkce. Jest má na to pěkné API, ale někdy nemusí fungovat tak, jak byste čekali. S tímto problémem jsem se setkal při testování side-effect importů — chtěl jsem simulovat, že první volání uspěje a druhé selže, pomocí jest.fn().mockResolvedValueOnce({your: ‘value’}).mockRejectedValue. Bohužel taková kombinace vůbec nefunguje a byl jsem nucen tyto dvě varianty rozdělit do samostatných testů.
Pár užitečných nástrojů pro úspěšnější testování softwaru
isolateModules a isolateModulesAsync
Pokud potřebujete testovat importovaný modul vícekrát, funkce isolateModules a isolateModulesAsync mohou pomoci. Vytváří sandbox prostředí ve vašem testu a zajišťují, že jednotlivé importy se vzájemně neovlivňují.
Testování více scénářů
Funkcionalita each umožňuje spustit daný test nebo sadu pro několik vstupů najednou — skvělá úspora času při testování více podobných scénářů.
Rozšíření match funkcí
Zajímavým rozšířením Jestu je knihovna jest-extended. Nabízí několik funkcí, které budete každodenně používat při psaní testů, a šetří vám úhozy.
Doufám, že při testování svých aplikací využijete alespoň některé z těchto tipů, a tím snížíte počet bugů v produkci. Přeji vám mnoho zachycených chyb a právem zasloužených zelených checkmarků v pipeline!