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ą opróżnione
- Wszystkie typy proste i kolekcje pozostaną bez zmian
- Wszystkie obiekty zostaną głęboko sklonowane
- Wszystkie instancje klas i ich prototypy zostaną głęboko sklonowane
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 node-request-interceptor. 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 { 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());
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
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Pamiętaj, że wywołania vi.mock
są podnoszone (hoistowane) na początek pliku. Nie umieszczaj wywołań vi.mock
wewnątrz beforeEach
, ponieważ tylko jedno z nich faktycznie zamockuje moduł.
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 vi.mock
i prototypem:
// 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 () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./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ść. Ta wartość nie jest automatycznie resetowana między 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ść, możesz użyć funkcji pomocniczej vi.stubEnv
z włączoną opcją konfiguracji unstubEnvs
(lub wywołać vi.unstubAllEnvs
ręcznie w hooku beforeEach
):
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,
},
};