Tato stránka je tahákem pro testování v JavaScriptu s použitím nástroje jménem Vitest. Jedná se o testovací framework, který je hodně podobný populárnímu frameworku jménem Jest.
Vyhneme se nekonečnému manuálnímu testování.
Po každé změně se naše testy mohou automaticky spustit, a zkontrolovat, jestli se něco nerozbilo
Psaní testů nás nutí psát čistší kód, protože se pro něj potom lépe vytvářejí testy.
Existují různé typy testů. Je jich více, ale ty nejznámější jsou pravděpodobně Unit testy, Integration testy a End-to-end testy.
Testování individuálních stavebních bloků aplikace. Každý stavební blok (unit) je testován zvlášť. Stavebním blokem je většinou funkce nebo třída, která nepoužívá jiné stavební bloky. Pokud dělám unit test, tak chceme testovat specifický stavební blok, jiné ne.
Testování kombinace stavebních bloků (unit). Testuje se jestli určité stavební bloky fungují dohromady. I když všechny stavební bloky fungují samostatně, tak se může stát, že pokud budou pracovat dohromady, tak dojde k selhání.
End-to-end nejsou nijak spjaty s Unit a Integration testy. Jejich úkolem je testovat kompletní funkcionalitu aplikace. Protože uživatelé používají celou aplikaci, ne jen její část.
Vždy bychom měli testovat jen vlastní kód. Pokud používáme nějaké knihovny třetích stran, tak ty netestujeme.
Měli bychom testovat vždy jen jednu věc. Neměli bychom v jednom testu testovat několik věcí najednou.
Pokud testujeme frontend aplikaci, tak bychom zároveň neměli testovat i backend. Backend můžeme testovat zvlášť.
Pro testování našeho kódu potřebujeme tři základní věci. Potřebujeme Test Runner, Assertion Library a samozřejmě kód aplikace, kterou chceme testovat.
K vytváření testů a jejich spouštění potřebujeme samozřejmě kód naší aplikace. Testování může být (a většinou i je) v naší aplikaci integrováno. Pokud jsme například naši aplikaci vytvořili pomocí nějakého nástroje postaveného třeba na Webpacku (např. Create React App), tak pro nás může být testování připraveno.
Test Runner představuje nástroj pro spouštění našich testů. Automaticky detekuje testovací kód, spouští jej a zobrazuje pro něj výsledky. Populárními test runnery jsou například Jest, Vitest nebo Karma.
Kromě test runneru potřebujeme k testování také Assertion Library. Tento nástroj slouží k definování výsledků, které po proběhnutím testu očekáváme. Assertion Library zjišťuje jestli se naše očekávání splní nebo ne. Populární Assertion knihovny jsou například Jest, Vitest nebo Chai.
AAA vzor se stal v testování skoro standardem. Rozděluje tvorbu testů na tři fáze: Arrange (připrav), Act (proveď) a Assert (vyhodnoť).
V této fázi si připravíme testovací prostředí a nadefinujeme hodnoty.
V této fázi provedeme testování. Spustíme nějaký kód, který chceme otestovat.
V této fázi vyhodnotíme výsledek testu. Porovnáme výslednou hodnotu s očekávanou hodnotou.
it("should summarize all numbers in an array", () => {
// arrange
const numbers = [1, 2, 3];
const expectedResult = 6;
// act
const result = add(number);
// assert
expect(result).toBe(expectedResult);
});
Ke spuštění testů můžeme použít příkaz 'vitest', který si můžeme nastavit jako test script v našem package.json souboru. Po jeho spuštění Vitest automaticky prohledá v našem projektu soubory s koncovkou .test.js nebo .spec.js a spustí je.
vitest | vitest watch - spouští testy ve watch módu
vitest run - spouští testy jen jednou
zobrazit možnostiVitest obsahuje funkce it a test, které nám umožňují vytvářet testy. Obě dělají stejnou věc. Jako první parametr berou popis testu a jako druhý funkci s testovacím kódem. V této funkci otestujeme nějaký kód a výsledek porovnáme s očekávaným výsledkem pomocí funkce expect.
import {expect, it} from 'vitest';
it("should summarize all number values in an array", () => {
const numbers = [1, 2, 3];
const expectedResult = 6;
// otestování kódu a získání výsledku
const result = add(numbers);
// porovnání výsledku s očekávaným výsledkem
expect(result).toBe(expectedResult);
});
Za funkci expect, do které předáváme hodnotu kterou chceme kontrolovat, můžeme řetězit spoustu metod, pomocí kterých můžeme předanou hodnotu zkontrolovat. Všechny můžete najít v dokumentaci.
Za funkci expect můžeme řetězit dvě podobné metody jménem toBe a toEqual. Rozdíl mezi nimi je ten, že toBe porovnává hodnoty přesně. Pokud chceme porovnávat dva objekty u kterých chceme zjistit jestli mají stejnou strukturu, tak je metoda toBe vyhodnotí jako rozdílné, protože jsou uloženy na jiném místě v paměti. Proto existuje metoda toEqual, která zjišťuje jestli se dvě hodnoty rovnají nebo mají stejnou strukturu.
import {expect, it} from 'vitest';
it("should be equal", () => {
const obj1 = {
someValue: true,
someOtherValue: 2
};
const obj2 = {
someValue: true,
someOtherValue: 2
};
// tento test selže (proměnná obj1 odkazuje na jiný objekt v paměti než obj2)
expect(obj1).toBe(obj2);
});
import {expect, it} from 'vitest';
it("should be equal", () => {
const obj1 = {
someValue: true,
someOtherValue: 2
};
const obj2 = {
someValue: true,
someOtherValue: 2
};
// tento test projde (obj1 má stejnou strukturu jako obj2)
expect(obj1).toEqual(obj2);
});
Vitest nám nabízí funkci describe, pomocí které můžeme v souboru seskupovat testy dohromady. To se může hodit, když například testujeme soubor obsahující různé funkce.
import {describe, expect, it} from 'vitest';
// seskupení testů pro funkci add
describe("add()", () => {
it("should do something...", () => {
// ... nějaký test ...
});
it("should do something too...", () => {
// ... nějaký test ...
});
});
// seskupení testů pro funkci substract
describe("substract()", () => {
it("should do something...", () => {
// ... nějaký test ...
});
// describe funkce můžeme i vnořovat
describe("something", () => {
// ... nějaké další testy ...
});
});
U některých funkcí v našem kódu můžeme v určitých situacích očekávat, že vyhodí chybu. Vitest nám to umožňuje otestovat.
it("should throw an error", () => {
const numbers = ["A", "B", "C"];
// nejjednodušší cesta jak otestovat jestli nějaká funkce vrací error
// je obalit její volání do jiné funkce a tu předat do expect funkce
// - jinak bychom mohli využít i try a catch
const resultFn = () => {
add(numbers);
}
// očekáváme že funkce vyhodí chybu
expect(resultFn).toThrow();
});
Při spouštění testů Vitest nečeká na to až se provedou callbacky a asynchronní kód. Proto může testovací funkce přijímat jako parametr funkci, kterou v testovací funkci zavoláme až budeme s testováním hotovi. Pokud ale testujeme kód, který používá promisy, tak je to o něco jednodušší.
import {expect, it} from 'vitest';
// testovací funkce jako parametr přijímá funkci, kterou zavoláme až budeme s testováním hotovi
it("should generate a token value", (done) => {
const testUserEmail = 'test@test.com';
generateToken(testUserEmail, (err, token) => {
// metoda toBeDefined (a podobné metody) může vrátit error, ale tady se nacházíme v
// callbacku, ne v it funkci, proto musíme její volání obalit do try catch bloku
try {
expect(token).toBeDefined();
// pokud metoda toBeDefined nevyhodila žádný error, tak můžeme done funkci zavolat bez parametrů
done();
} catch(err) {
// jako parametr předáváme do done funkce error, který metoda toBeDefined vyhodila
done(err);
}
});
});
import {expect, it} from 'vitest';
it("should generate a token value", () => {
const testUserEmail = 'test@test.com';
// pokud testovaná funkce vrací promise, tak jej můžeme předat do expect funkce
// - když předáváme do expect funkce promise, tak za ni můžeme řetězit resolves (pokud očekáváme že se promise splní)
// nebo rejects (pokud očekáváme že se promise nesplní) a nakonec podmínku pro hodnotu s kterou se promise splní/nesplní
// - funkce expect, která přijímá jako parametr promise, se musí v testovací funkci vrátit pomocí return
return expect(generateToken(testUserEmail)).resolves.toBeDefined();
});
// pokud chceme používat async await, tak můžeme, testování je díky tomu ještě jednodušší
it("should generate a token value - v2", async () => { // testovací funkce je definována jako async (automaticky vrací promise)
const testUserEmail = 'test@test.com';
const token = await generateTokenPromise(testUserEmail);
expect(token).toBeDefined();
});
Pokud používáme nějakou hodnotu ve více testech (v každém testu ji vytváříme), tak ji můžeme deklarovat mimo testy a v testech ji jen použít. Takto to ale můžeme udělat jen pro hodnoty, které se nemění. Jinak bychom museli před každém spuštění testu hodnotu resetovat. K tomuto účelu složí hooks. Můžeme s jejich pomocí spustit nějaký kód před/po každým/všemi testem.
import {beforeEach, afterEach, beforeAll, afterAll, describe} from 'vitest';
beforeEach(() => {
// kód, který se spustí před každým testem
});
afterEach(() => {
// kód, který se spustí po každém testu
});
beforeAll(() => {
// kód, který se spustí před všemi testy
});
afterAll(() => {
// kód, který se spustí po provedení všech testů
});
// hooks můžeme také vnořovat do describe funkcí - budou se týkat jen testů, které describe funkce obsahuje
describe("add()", {
beforeEach(() => { /* nějaký kód */ });
});
Vitest nám umožňuje vytvořit si funkci, kterou můžeme použít jako obal nebo prázdnou náhradu pro jinou funkci. Máme s její pomocí možnost například zjistit, jestli byla funkce volána když ji předáme jako parametr testované funkci.
import {it, expect, vi} from 'vitest';
it("should call passed function", () => {
// vytvoření špionážní funkce
const callback = vi.fn();
// předání špionážní funkce jako parametr testované funkci
generateData(callback);
// očekáváme že se funkce callback ve funkci generateData zavolala
expect(callback).toBeCalled();
// dále můžeme po předání špionážní funkce do expect funkce řetězit metody:
// toBeCalledTimes, toBeCalledWith, atd... (více v dokumentaci)
});
více v dokumentaci
Vitest nám umožňuje nahradit části API jiným kódem. Při testování například nechceme, aby funkce která něco ukládá na disk opravdu něco na disk ukládala. Chceme to jen otestovat. Proto můžeme třeba změnit funkcionalitu funkcí, které testovaná funkce používá k zápisu dat na disk.
import {it, expect, vi} from 'vitest';
import {promises as fs} from 'fs';
// všechny funkce v modulu fs se změní v prázdné funkce
vi.mock(fs);
it('should execute the writeFile method', () => {
const testData = 'Test';
const testFilename = 'test.txt';
// otestování funkce writeData
writeData(testData, testFilename);
// očekáváme že se funkce writeFile ve funkci writeData zavolá
expect(fs.writeFile).toBeCalled();
});
Pokud chceme pro nějakou funkci v modulu nastavit jinou než prázdnou funkci, tak to můžeme udělat následující způsobem:
vi.mock(path, () => {
return {
default: {
// náhrada pro funkci join v modulu path
join: (...args) => {
return args[args.length - 1];
}
}
}
});
Pokud chceme mockovat stejný module ve více souborech, tak kód pro jeho mockování můžeme přesunout do samostatného souboru. Vitest nám umožňuje vytvořit si složku __mocks__, kde můžeme vytvářet soubory pojmenované jako názvy modulů (třeba fs.js). V těchto souborech můžeme exportovat mocknutou funkcionalitu, kterou chceme používat když v jiných souborech zavoláme vi.mock.
// __mocks__/fs.js
import { vi } from 'vitest';
// mocknutí části modulu fs jménem promises
export const promises = {
// náhrada pro funkci writeFile
writeFile: vi.fn((path, data) => {
return new Promise((resolve, reject) => {
resolve();
});
})
}
Pokud chceme mocknout nějakou globální funkci, tak nám Vitest poskytuje kromě metody mock také metodu stubGlobal, kterou k tomu můžeme využít.
const testFetch = vi.fn();
// mocknutí globální funkce jménem fetch
vi.stubGlobal("fetch", testFetch);
Pokud chceme testovat DOM, tak nejprve musíme změnit prostředí, ve kterém se náš testovací kód spouští. Máme na výběr z těchto tří možností:
Tato možnost je nastavena jako defaultní. V tomto prostředí jsou k dispozici NodeJS API a moduly.
Prostředí pro testování frontend kódu. K dispozici také třeba v testovacím frameworku jménem Jest.
Další prostředí pro testování frontend kódu.
Prostředí ve kterém se naše testy spustí můžeme nastavit pomocí možnosti --environment:
vitest --environment happy-dom
Předtím než začneme s testováním, tak musíme načíst stránku, kterou chceme testovat. To můžeme udělat následujícím způsobem (používáme happy-dom):
import fs from 'fs';
import path from 'path';
import { Window } from 'happy-dom';
// získání cesty k HTML souboru
const htmlDocPath = path.join(process.cwd(), 'index.html');
// přečtení HTML souboru a uložení jeho obsahu do proměnné
const htmlDocumentContent = fs.readFileSync(htmlDocPath).toString();
// vytvoření nového window objektu
const window = new Window();
// získání documentu window objektu
const document = window.document;
// zapsání obsahu HTML souboru do documentu
document.write(htmlDocumentContent);
// změnění documentu na document virtuálního DOMu
vi.stubGlobal('document', document);
Následující ukázka ukazuje, jakým způsobem můžeme DOM testovat.
/* ... kód z minulé ukázky a importy ... */
// při DOM testování bychom měli virtual DOM před každým testem resetovat
beforeEach(() => {
// smazání obsahu documentu
document.body.innerHTML = '';
// zapsání obsahu HTML souboru do documentu
document.write(htmlDocumentContent);
// stubGlobal volat nemusíme, protože document je objekt a ten se předává adresou
});
it('should add new element to the id="todo-list" element', () => {
// spuštění testované funkce
addItem("create website");
// získání elementu s ID todo-list
const todoList = document.getElementById("todo-list");
// získání prvního elementu z elementu s ID todo-list
const liElement = todoList.firstElementChild;
// očekáváme že se do elementu s ID todo-list přidal nový element
expect(liElement).not.toBeNull();
});
Pro testování DOMu nám JSDOM nebo Happy-DOM nemusí stačit. Proto existují různé knihovny pro DOM testování. Jednou z nich je třeba Testing Library, pomocí které můžeme jednoduše testovat i populární frontend frameworky jako je React.