Mockolás
Tesztek írásakor előbb-utóbb szükségessé válik belső vagy külső szolgáltatások "hamis" verziójának létrehozása. Ezt általában mockolásnak nevezzük. A Vitest segédfüggvényeket biztosít a vi segítővel. Importálhatod a import { vi } from 'vitest'
használatával, vagy globálisan is elérheted (ha a globális konfiguráció engedélyezve van).
WARNING
A mockok állapotváltozásainak elkerülése érdekében ne felejtsd el törölni vagy visszaállítani a mockokat minden tesztfuttatás előtt vagy után! További információért lásd a mockReset
dokumentációt.
Ha azonnal belevágnál, nézd meg az [API szekciót]; ha nem, olvass tovább, hogy mélyebben belemerülj a mockolás világába.
Dátumok
Néha szükség lehet a dátum kontrollálására, hogy biztosítsd a konzisztenciát a tesztelés során. A Vitest a @sinonjs/fake-timers
csomagot használja az időzítők, valamint a rendszerdátum manipulálására. Az API-ról részletesebben itt találsz információt.
Példa
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const businessHours = [9, 17];
function purchase() {
const currentHour = new Date().getHours();
const [open, close] = businessHours;
if (currentHour > open && currentHour < close) return { message: 'Success' };
return { message: 'Error' };
}
describe('purchasing flow', () => {
beforeEach(() => {
// tell vitest we use mocked time
vi.useFakeTimers();
});
afterEach(() => {
// restoring date after each test run
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// set hour within business hours
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// set hour outside business hours
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Error' });
});
});
Függvények
A függvények mockolása két különböző kategóriába sorolható: kémkedés (spying) és mockolás.
Néha csak azt kell ellenőrizned, hogy egy adott függvény meghívásra került-e (és esetleg milyen argumentumokkal). Ezekben az esetekben egy kém (spy) lenne minden, amire szükséged van, amelyet közvetlenül a vi.spyOn()
segítségével használhatsz (további információ itt).
A spy-ok azonban csak a függvények megfigyelésében segíthetnek, nem képesek megváltoztatni ezen függvények implementációját. Abban az esetben, ha egy függvény hamis (vagy mockolt) verzióját kell létrehoznunk, használhatjuk a vi.fn()
-t (további információ itt).
A Tinyspy-t használjuk a függvények mockolásának alapjaként, de van saját csomagunk, hogy jest
kompatibilis legyen. Mind a vi.fn()
, mind a vi.spyOn()
ugyanazokat a metódusokat osztja meg, azonban csak a vi.fn()
visszatérési értéke hívható.
Példa
import { afterEach, describe, expect, it, vi } from 'vitest';
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // can also be a `getter or setter if supported`
};
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest');
expect(spy.getMockName()).toEqual('getLatest');
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1]
);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementationOnce(() => 'access-restricted');
expect(messages.getLatest()).toEqual('access-restricted');
expect(spy).toHaveBeenCalledTimes(2);
});
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(1);
mock.mockImplementationOnce(() => 'access-restricted');
expect(mock()).toEqual('access-restricted');
expect(mock).toHaveBeenCalledTimes(2);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(3);
});
});
További információk
Globális változók
A vi.stubGlobal
segítővel mockolhatod azokat a globális változókat, amelyek nincsenek jelen a jsdom
-ban vagy a node
-ban. Ez a globális változó értékét a globalThis
objektumba helyezi.
import { vi } from 'vitest';
const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
// now you can access it as `IntersectionObserver` or `window.IntersectionObserver`
Modulok
A modulok mockolása lehetővé teszi a harmadik féltől származó könyvtárak használatának megfigyelését, az argumentumok és a kimenet tesztelését, vagy akár az implementáció újradefiniálását.
A részletes API leírásért lásd a vi.mock()
API szekciót.
Automockoló algoritmus
Ha a kódod egy mockolt modult importál anélkül, hogy tartozna hozzá __mocks__
fájl vagy factory
, a Vitest magát a modult fogja mockolni a meghívásával és az összes export mockolásával.
A következő elvek érvényesek:
- Minden tömb kiürül
- Minden primitív és gyűjtemény változatlan marad
- Minden objektum mélyen klónozva lesz
- Az osztályok és prototípusaik minden példánya mélyen klónozva lesz
Példa
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// handlers
export function success(data) {}
export function failure(data) {}
// get todos
export async function getTodos(event, context) {
const client = new Client({
// ...clientOptions
});
await client.connect();
try {
const result = await client.query('SELECT * FROM todos;');
client.end();
return success({
message: `${result.rowCount} item(s) returned`,
data: result.rows,
status: true,
});
} catch (e) {
console.error(e.stack);
client.end();
return failure({ message: e, status: false });
}
}
vi.mock('pg', () => {
const Client = vi.fn();
Client.prototype.connect = vi.fn();
Client.prototype.query = vi.fn();
Client.prototype.end = vi.fn();
return { Client };
});
vi.mock('./handlers.js', () => {
return {
success: vi.fn(),
failure: vi.fn(),
};
});
describe('teendőlista lekérése', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('sikeresen vissza kell adnia az elemeket.', async () => {
client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(success).toBeCalledWith({
message: '0 item(s) returned',
data: [],
status: true,
});
});
it('hibát kell dobnia.', async () => {
const mError = new Error('Unable to retrieve rows');
client.query.mockRejectedValueOnce(mError);
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(failure).toBeCalledWith({ message: mError, status: false });
});
});
Kérések
Mivel a Vitest Node-ban fut, a hálózati kérések mockolása nehézkes; a webes API-k nem érhetők el, ezért szükségünk van valamire, ami utánozza a hálózati viselkedést. Ehhez a Mock Service Worker használatát javasoljuk. Ez lehetővé teszi mind a REST
, mind a GraphQL
hálózati kérések mockolását, és keretrendszer-független.
A Mock Service Worker (MSW) úgy működik, hogy elfogja a tesztek által végrehajtott kéréseket, lehetővé téve a használatát anélkül, hogy meg kellene változtatnod az alkalmazáskódodat. Böngészőben ez a Service Worker API-t használja. A Node.js-ben és a Vitest esetében a node-request-interceptor-t használja. Ha többet szeretnél megtudni az MSW-ről, olvasd el a bevezetőjüket.
Konfiguráció
Az alábbiak szerint használhatod a setup fájlban:
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, rest } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
rest.get('https://rest-endpoint.example/path/to/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(posts));
}),
];
const graphqlHandlers = [
graphql.query(
'https://graphql-endpoint.example/api/v1/posts',
(req, res, ctx) => {
return res(ctx.data(posts));
}
),
];
const server = setupServer(...restHandlers, ...graphqlHandlers);
// Start server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Close server after all tests
afterAll(() => server.close());
// Reset handlers after each test `important for test isolation`
afterEach(() => server.resetHandlers());
A szerver
onUnhandleRequest: 'error'
beállítással történő konfigurálása biztosítja, hogy hiba keletkezzen, ha olyan kérés érkezik, amelyhez nincs definiálva kéréskezelő.
Példa
Van egy teljes működő példánk, amely MSW-t használ: React Testing with MSW.
További információk
Az MSW sokkal többet tud. Hozzáférhetsz a cookie-khoz és a lekérdezési paraméterekhez, definiálhatsz mock hiba válaszokat és még sok mást! Ha mindent meg szeretnél tudni, amit az MSW-vel tehetsz, olvasd el a dokumentációjukat.
Időzítők
Amikor időzítéseket vagy intervallumokat használó kódot tesztelünk, ahelyett, hogy a tesztek várakoznának vagy időtúllépést okoznának, felgyorsíthatjuk a tesztelést a setTimeout
és a setInterval
hívásokat mockoló "hamis" időzítőkkel.
A részletesebb API leíráshoz lásd a vi.useFakeTimers
API szekciót.
Példa
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 hours
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minute
}
const mock = vi.fn(() => console.log('executed'));
describe('késleltetett végrehajtás.', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('végre kell hajtania a függvényt.', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('nem szabad végrehajtania a függvényt.', () => {
executeAfterTwoHours(mock);
// advancing by 2ms won't trigger the func
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('percenként végre kell hajtania.', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Segédlet
INFO
Az alábbi példákban a vi
közvetlenül a vitest
-ből van importálva. Használhatod globálisan is, ha a konfigurációban a globals
értékét true
-ra állítod.
A következőt szeretném…
- A
method
metódus kémlelése
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Exportált változók mockolása
// some-path.js
export const getter = 'variable';
// some-path.test.ts
import * as exports from './some-path.js';
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');
- Exportált függvény mockolása
Példa vi.mock
használatával:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Ne felejtsd el, hogy a vi.mock
hívás a fájl elejére kerül (hoisting). Ne helyezz vi.mock
hívásokat a beforeEach
blokkba, mert csak az egyik fogja ténylegesen mock-olni a modult.
Példa vi.spyOn
használatával:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Exportált osztály implementációjának mockolása
Példa vi.mock
és prototípus használatával:
// some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// A SomeClass.mock.instances tartalmazni fogja a SomeClass példányait
Példa vi.mock
és visszatérési érték használatával:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// A SomeClass.mock.results tartalmazni fogja a visszaadott objektumokat
Példa vi.spyOn
használatával:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// bármi, ami az első két példában szerepel
});
- Egy függvény által visszaadott objektum kémlelése
Példa gyorsítótár (cache) használatával:
// some-path.ts
export function useObject() {
return { method: () => true };
}
// useObject.js
import { useObject } from './some-path.js';
const obj = useObject();
obj.method();
// useObject.test.js
import { useObject } from './some-path.js';
vi.mock('./some-path.js', () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// a useObject() minden hívásakor
// ugyanazt az objektum referenciát adja vissza
return _cache;
};
return { useObject };
});
const obj = useObject();
// az `obj.method` meghívásra került a `some-path` modulban
expect(obj.method).toHaveBeenCalled();
- Egy modul egy részének mockolása
import { mocked, original } from './some-path.js';
vi.mock('./some-path.js', async () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./some-path.js'
);
return {
...mod,
mocked: vi.fn(),
};
});
original(); // eredeti viselkedés
mocked(); // egy kémlelő függvény
- Az aktuális dátum mockolása
A Date
időpontjának mockolásához használhatod a vi.setSystemTime
segédfüggvényt. Ez az érték nem fog automatikusan visszaállni a különböző tesztek között.
Ne feledd, hogy a vi.useFakeTimers
használata szintén megváltoztatja a Date
időpontját.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// a mockolt idő visszaállítása
vi.useRealTimers();
- Globális változó mockolása
Globális változót beállíthatsz érték hozzárendelésével a globalThis
-hez, vagy a vi.stubGlobal
segédfüggvény használatával. A vi.stubGlobal
használatakor ez nem fog automatikusan visszaállni a különböző tesztek között, hacsak nem engedélyezed az unstubGlobals
konfigurációs opciót, vagy nem hívod meg a vi.unstubAllGlobals
függvényt.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- Az
import.meta.env
mockolása
A környezeti változó megváltoztatásához egyszerűen hozzárendelhetsz egy új értéket. Ez az érték nem fog automatikusan visszaállni a különböző tesztek között.
import { beforeEach, expect, it } from 'vitest';
// manuálisan visszaállíthatod a `beforeEach` hook-ban
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('changes value', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
Ha azt szeretnéd, hogy az érték automatikusan visszaállításra kerüljön, használhatod a vi.stubEnv
segédfüggvényt az engedélyezett unstubEnvs
konfigurációs opcióval (vagy manuálisan meghívhatod a vi.unstubAllEnvs
függvényt a beforeEach
hook-ban):
import { expect, it, vi } from 'vitest';
// a tesztek futtatása előtt a `VITE_ENV` értéke `test`
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('az érték visszaállításra kerül egy másik teszt futtatása előtt', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};