Mockowanie
Podczas pisania testów prędzej czy później pojawi się potrzeba stworzenia "fałszywej" wersji zależności wewnętrznej lub zewnętrznej. Jest to powszechnie nazywane mockowaniem. Vitest udostępnia funkcje pomocnicze, które ułatwiają ten proces za pomocą obiektu vi. Możesz użyć import { vi } from 'vitest'
lub uzyskać do niego dostęp globalnie (gdy konfiguracja globalna jest włączona).
WARNING
Zawsze pamiętaj, aby wyczyścić lub przywrócić mocki przed lub po każdym teście, aby uniknąć niepożądanych zmian stanu między testami! Więcej informacji znajdziesz w dokumentacji mockReset
.
Jeśli chcesz od razu przejść do konkretnych rozwiązań, sprawdź sekcję API; w przeciwnym razie kontynuuj czytanie, aby dowiedzieć się więcej o mockowaniu.
Daty
Czasami konieczne jest kontrolowanie daty, aby zapewnić spójność testów. Vitest używa pakietu @sinonjs/fake-timers
do manipulowania czasomierzami, a także datą systemową. Szczegółowe informacje na temat konkretnego API można znaleźć tutaj.
Przykład
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' });
});
});
Funkcje
Mockowanie funkcji dzieli się na dwie kategorie: szpiegowanie (ang. spying) i mockowanie (ang. mocking).
Czasami wystarczy sprawdzić, czy dana funkcja została wywołana (i ewentualnie z jakimi argumentami). W takich przypadkach szpiegowanie jest wystarczające i można je zrealizować bezpośrednio za pomocą vi.spyOn()
(więcej informacji tutaj).
Jednak szpiedzy (ang. spies) służą tylko do obserwowania funkcji, nie mogą zmieniać ich implementacji. Jeśli potrzebujemy utworzyć fałszywą (lub mockowaną) wersję funkcji, możemy użyć vi.fn()
(więcej informacji tutaj).
Używamy Tinyspy jako podstawy do mockowania funkcji, ale mamy własną otoczkę, aby zapewnić kompatybilność z jest
. Zarówno vi.fn()
i vi.spyOn()
współdzielą te same metody, jednak tylko wynik zwracany przez vi.fn()
jest wywoływalny.
Przykład
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);
});
});
Więcej
Zmienne globalne
Możesz mockować zmienne globalne, które nie są dostępne w jsdom lub node, używając pomocnika vi.stubGlobal
. Umieści on wartość zmiennej globalnej w obiekcie globalThis
.
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`
Moduły
Mockowanie modułów pozwala obserwować biblioteki firm trzecich, które są wywoływane w kodzie, umożliwiając testowanie argumentów, wyników, a nawet ponowne definiowanie ich implementacji.
Zobacz sekcję API vi.mock()
, aby uzyskać bardziej szczegółowy opis API.
Algorytm automatycznego mockowania
Jeśli twój kod importuje mockowany moduł, bez powiązanego pliku __mocks__
lub factory
dla tego modułu, Vitest automatycznie zamockuje moduł, wywołując go i mockując każdy eksport.
Obowiązują następujące zasady:
- Wszystkie tablice zostaną wyczyszczone.
- Wszystkie typy proste (primitive) i kolekcje pozostaną bez zmian.
- Wszystkie obiekty zostaną głęboko sklonowane.
- Wszystkie instancje klas i ich prototypy zostaną głęboko sklonowane.
Moduły wirtualne
Vitest obsługuje mockowanie wirtualnych modułów Vite (https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention). Działa to inaczej niż w przypadku modułów wirtualnych w Jest. Zamiast przekazywać virtual: true
do funkcji vi.mock
, należy poinformować Vite, że moduł istnieje, w przeciwnym razie parsowanie zakończy się niepowodzeniem. Można to zrobić na kilka sposobów:
- Dostarczenie aliasu
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Dostarczenie wtyczki, która rozwiązuje moduł wirtualny
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') return 'virtual:$app/forms';
},
},
],
};
Zaletą drugiego podejścia jest możliwość dynamicznego tworzenia różnych wirtualnych punktów wejścia. Jeśli przekierujesz kilka modułów wirtualnych do jednego pliku, wszystkie zostaną dotknięte przez vi.mock
, dlatego upewnij się, że używasz unikalnych identyfikatorów.
Pułapki mockowania
Należy pamiętać, że nie można mockować wywołań metod, które są wywoływane wewnątrz innych metod tego samego pliku. Na przykład w tym kodzie:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
Nie można mockować metody foo
z zewnątrz, ponieważ jest do niej bezpośrednie odwołanie. Zatem ten kod nie będzie miał wpływu na wywołanie foo
wewnątrz foobar
(ale wpłynie na wywołanie foo
w innych modułach):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// to wpłynie tylko na "foo" poza oryginalnym modułem
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// to wpłynie tylko na "foo" poza oryginalnym modułem
foo: () => 'mocked',
};
});
Można potwierdzić to zachowanie, dostarczając implementację bezpośrednio do metody foobar
:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// eksportowane foo odwołuje się do mockowanej metody
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo !== foo; // false
}
Takie jest zamierzone zachowanie. Zwykle jest to oznaka złego kodu, gdy mockowanie jest zaangażowane w taki sposób. Rozważ refaktoryzację kodu na wiele plików lub ulepszenie architektury aplikacji przy użyciu technik takich jak wstrzykiwanie zależności.
Przykład
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} element(ów) zwróconych`,
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('get a list of todo items', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return items successfully', 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('should throw an error', 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 });
});
});
Żądania
Ponieważ Vitest działa w Node.js, mockowanie żądań sieciowych jest utrudnione; interfejsy API przeglądarki nie są dostępne, więc potrzebujemy czegoś, co będzie naśladować zachowanie sieci. Zalecamy Mock Service Worker do tego celu. Pozwoli to na mockowanie zarówno żądań sieciowych REST
, jak i GraphQL
i jest niezależne od frameworka.
Mock Service Worker (MSW) działa poprzez przechwytywanie żądań wykonywanych przez testy, co pozwala na używanie go bez zmiany kodu aplikacji. W przeglądarce używa Service Worker API. W Node.js i dla Vitest używa @mswjs/interceptors
. Aby dowiedzieć się więcej o MSW, przeczytaj wprowadzenie
Konfiguracja
Możesz go użyć w swoim pliku konfiguracyjnym w następujący sposób:
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { HttpResponse, graphql, http } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
http.get('https://rest-endpoint.example/path/to/posts', () => {
return HttpResponse.json(posts);
}),
];
const graphqlHandlers = [
graphql.query('ListPosts', () => {
return HttpResponse.json({
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());
Skonfigurowanie serwera z
onUnhandleRequest: 'error'
zapewnia, że zostanie zgłoszony błąd, gdy tylko pojawi się żądanie, dla którego nie zdefiniowano odpowiedniego handlera.
Przykład
Mamy w pełni działający przykład, który używa MSW: React Testing with MSW.
Więcej
MSW ma o wiele więcej do zaoferowania. Możesz uzyskać dostęp do plików cookie i parametrów zapytania, definiować mockowane odpowiedzi błędów i wiele więcej! Aby zobaczyć wszystko, co możesz zrobić z MSW, przeczytaj ich dokumentację.
Timery
Podczas testowania kodu zawierającego limity czasu lub interwały, zamiast czekać na zakończenie testów lub przekroczenie limitu czasu, możemy przyspieszyć nasze testy, używając mockowanych timerów, które mockują wywołania setTimeout
i setInterval
.
Zobacz sekcję API vi.useFakeTimers
, aby uzyskać bardziej szczegółowy opis API.
Przykład
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('delayed execution', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should execute the function', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should not execute the function', () => {
executeAfterTwoHours(mock);
// advancing by 2ms won't trigger the func
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('should execute every minute', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Podręczny zestaw
INFO
vi
w poniższych przykładach jest importowane bezpośrednio z vitest
. Możesz również używać go globalnie, jeśli ustawisz opcję globals
na true
w swojej konfiguracji.
Chcę…
Szpiegować metodę
const instance = new SomeClass();
vi.spyOn(instance, 'method');
Mockować eksportowane zmienne
// 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');
Mockować eksportowaną funkcję
- Przykład z użyciem
vi.mock
:
WARNING
Pamiętaj, że wywołanie vi.mock
jest przenoszone na początek pliku. Zawsze będzie wykonywane przed wszystkimi importami.
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
- Przykład z użyciem
vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
Mockować implementację eksportowanej klasy
- Przykład z użyciem
vi.mock
i.prototype
:
// 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 };
});
// SomeClass.mock.instances będzie zawierać instancje SomeClass
- Przykład z
vi.mock
i wartością zwracaną:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.results będzie zawierać zwrócone obiekty
- Przykład z
vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// cokolwiek pasuje z dwóch pierwszych przykładów
});
Szpiegować obiekt zwracany z funkcji
- Przykład użycia cache:
// 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(),
};
}
// teraz każde wywołanie useObject() zwróci tę samą referencję do obiektu
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method została wywołana wewnątrz some-path
expect(obj.method).toHaveBeenCalled();
Mockować część modułu
import { mocked, original } from './some-path.js';
vi.mock('./some-path.js', async importOriginal => {
const mod = await importOriginal<typeof import('./some-path.js')>();
return {
...mod,
mocked: vi.fn(),
};
});
original(); // ma oryginalne zachowanie
mocked(); // jest funkcją szpiegującą
Mockować aktualną datę
Aby zamockować czas Date
, możesz użyć funkcji pomocniczej vi.setSystemTime
. Ta wartość nie jest automatycznie resetowana między testami.
Pamiętaj, że użycie vi.useFakeTimers
również wpływa na czas Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// resetowanie zamockowanego czasu
vi.useRealTimers();
Mockować zmienną globalną
Możesz ustawić zmienną globalną, przypisując wartość do globalThis
lub używając funkcji pomocniczej vi.stubGlobal
. Używając vi.stubGlobal
, wartość nie jest automatycznie resetowana między testami, chyba że włączysz opcję konfiguracji unstubGlobals
lub wywołasz vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
Mockować import.meta.env
- Aby zmienić zmienną środowiskową, możesz po prostu przypisać jej nową wartość.
WARNING
Wartość zmiennej środowiskowej nie zostanie automatycznie zresetowana między różnymi testami.
import { beforeEach, expect, it } from 'vitest';
// możesz zresetować ją ręcznie w hooku beforeEach
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('zmienia wartość', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
- Jeśli chcesz automatycznie zresetować wartości, możesz użyć pomocnika
vi.stubEnv
z włączoną opcją konfiguracjiunstubEnvs
(lub wywołać ręcznievi.unstubAllEnvs
](/api/vi#vi-unstuballenvs) w hookubeforeEach
):
import { expect, it, vi } from 'vitest';
// przed uruchomieniem testów "VITE_ENV" ma wartość "test"
import.meta.env.VITE_ENV === 'test';
it('zmienia wartość', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('wartość jest przywracana przed uruchomieniem kolejnego testu', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};