Mocking
Beim Schreiben von Tests dauert es nicht lange, bis du eine simulierte Version eines internen oder externen Dienstes erstellen musst. Dies wird üblicherweise als Mocking bezeichnet. Vitest bietet Hilfsfunktionen, die dir dabei helfen, und zwar über den Helfer vi
. Du kannst import { vi } from 'vitest'
verwenden oder global darauf zugreifen, wenn die Option globals
in der Konfiguration aktiviert ist.
WARNING
Denke immer daran, Mocks vor oder nach jedem Testlauf zurückzusetzen oder wiederherzustellen, um unerwünschte Zustandsänderungen zwischen den Testläufen zu vermeiden! Weitere Informationen findest du in der Dokumentation zu mockReset
.
Wenn du direkt loslegen möchtest, schau dir den API-Bereich an. Ansonsten lies weiter, um tiefer in das Thema Mocking einzutauchen.
Dates (Datumsangaben)
Manchmal ist es notwendig, das Datum zu manipulieren, um die Konsistenz beim Testen zu gewährleisten. Vitest verwendet das Paket @sinonjs/fake-timers
zur Manipulation von Timern und des Systemdatums. Detaillierte Informationen zur spezifischen API findest du hier.
Beispiel
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(() => {
// Vitest mitteilen, dass wir die simulierte Zeit verwenden
vi.useFakeTimers();
});
afterEach(() => {
// Datum nach jedem Testlauf wiederherstellen
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// Stunde innerhalb der Geschäftszeiten setzen
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// Der Zugriff auf Date.now() liefert das oben gesetzte Datum
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// Stunde außerhalb der Geschäftszeiten setzen
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// Der Zugriff auf Date.now() liefert das oben gesetzte Datum
expect(purchase()).toEqual({ message: 'Error' });
});
});
Functions (Funktionen)
Das Mocking von Funktionen lässt sich in zwei Kategorien unterteilen: Spionage (Spying) & Mocks.
Manchmal musst du nur überprüfen, ob eine bestimmte Funktion aufgerufen wurde (und gegebenenfalls welche Argumente übergeben wurden). In diesen Fällen reicht ein Spy aus, den du direkt mit vi.spyOn()
erstellen kannst (mehr dazu hier).
Spies helfen dir jedoch nur, Funktionen zu überwachen. Sie können die Implementierung dieser Funktionen nicht ändern. Wenn du eine gefälschte (oder gemockte) Version einer Funktion erstellen musst, kannst du vi.fn()
verwenden (mehr dazu hier).
Wir verwenden Tinyspy als Basis für das Mocking von Funktionen, haben aber einen eigenen Wrapper, um die Kompatibilität mit jest
zu gewährleisten. Sowohl vi.fn()
als auch vi.spyOn()
haben die gleichen Methoden, aber nur das Ergebnis von vi.fn()
ist aufrufbar.
Beispiel
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, // kann auch ein Getter oder Setter sein, falls unterstützt
};
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);
});
});
Mehr
Globals
Du kannst globale Variablen mocken, die mit jsdom
oder node
nicht vorhanden sind, indem du den Helfer vi.stubGlobal
verwendest. Er setzt den Wert der globalen Variablen im globalThis
-Objekt.
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`
Modules (Module)
Mock-Module werden verwendet, um Bibliotheken von Drittanbietern zu simulieren, die in deinem Code verwendet werden. Dadurch kannst du Argumente und Ausgaben testen oder sogar die Implementierung der Module ändern.
Weitere Informationen findest du im API-Abschnitt vi.mock()
für eine detailliertere API-Beschreibung.
Automocking (Automatisches Mocking)
Wenn dein Code ein Modul importiert, für das keine zugehörige __mocks__
-Datei oder factory
existiert, mockt Vitest das Modul automatisch, indem es es aufruft und jeden Export mockt.
Dabei gelten folgende Prinzipien:
- Alle Arrays werden geleert
- Alle primitiven Datentypen und Sammlungen bleiben unverändert
- Alle Objekte werden tief geklont
- Alle Instanzen von Klassen und ihre Prototypen werden tief geklont
Beispiel
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('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 });
});
});
Requests (Anfragen)
Da Vitest in Node ausgeführt wird, ist das Mocking von Netzwerkanfragen schwierig. Web-APIs sind nicht verfügbar, daher benötigen wir etwas, das das Netzwerkverhalten für uns simuliert. Wir empfehlen Mock Service Worker, um dies zu erreichen. Damit kannst du sowohl REST
- als auch GraphQL
-Netzwerkanfragen mocken, und es ist Framework-unabhängig.
Mock Service Worker (MSW) fängt die Anfragen ab, die deine Tests stellen, sodass du es verwenden kannst, ohne deinen Anwendungscode zu ändern. Im Browser verwendet dies die Service Worker API. In Node.js und für Vitest verwendet es node-request-interceptor. Um mehr über MSW zu erfahren, lies ihre Einführung.
Configuration (Konfiguration)
Du kannst es wie unten in deiner Setup-Datei verwenden:
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);
// Server vor allen Tests starten
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Server nach allen Tests schließen
afterAll(() => server.close());
// Handler nach jedem Test zurücksetzen (wichtig für die Testisolation)
afterEach(() => server.resetHandlers());
Die Konfiguration des Servers mit
onUnhandledRequest: 'error'
stellt sicher, dass ein Fehler ausgelöst wird, wenn eine Anfrage vorliegt, für die kein entsprechender Handler definiert ist.
Beispiel
Wir haben ein vollständiges, funktionierendes Beispiel, das MSW verwendet: React Testing with MSW.
Mehr
Es gibt noch viel mehr zu MSW. Du kannst auf Cookies und Abfrageparameter zugreifen, Mock-Fehlerantworten definieren und vieles mehr! Um alles zu sehen, was du mit MSW machen kannst, lies ihre Dokumentation.
Timers (Timer)
Wenn wir Code testen, der Timeouts oder Intervalle verwendet, können wir unsere Tests beschleunigen, indem wir "Fake"-Timer verwenden, die Aufrufe von setTimeout
und setInterval
simulieren, anstatt auf die tatsächliche Zeit zu warten oder ein Timeout zu riskieren.
Weitere Informationen findest du im API-Abschnitt vi.useFakeTimers
für eine detailliertere API-Beschreibung.
Beispiel
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);
});
});
Kurzübersicht
INFO
vi
in den Beispielen unten wird direkt von vitest
importiert. Du kannst es auch global verwenden, wenn du globals
in deiner Konfiguration auf true
setzt.
Ziele:
- Eine
Methode
ausspionieren
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Exportierte Variablen simulieren
// 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');
- Exportierte Funktion simulieren
Beispiel mit vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Beachte, dass der vi.mock
-Aufruf an den Anfang der Datei verschoben wird. Platziere keine vi.mock
-Aufrufe innerhalb von beforeEach
. Nur der erste dieser Aufrufe wird ein Modul mocken.
Beispiel mit vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Exportierte Klassenimplementierung simulieren
Beispiel mit vi.mock
und Prototyp:
// 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 wird SomeClass enthalten
Beispiel mit vi.mock
und Rückgabewert:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns wird das zurückgegebene Objekt enthalten
Beispiel mit vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// was auch immer für deine Zwecke geeignet ist
});
- Ein von einer Funktion zurückgegebenes Objekt ausspionieren
Beispiel mit Cache-Nutzung:
// 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(),
};
}
// Dadurch wird bei jedem Aufruf von `useObject()` dieselbe Objektreferenz zurückgegeben.
return _cache;
};
return { useObject };
});
const obj = useObject();
// Die Methode obj.method wurde in some-path aufgerufen
expect(obj.method).toHaveBeenCalled();
- Einen Teil eines Moduls simulieren
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(); // hat das ursprüngliche Verhalten
mocked(); // ist eine Spy-Funktion
- Das aktuelle Datum simulieren
Um die Zeit von Date
zu simulieren, kannst du die Hilfsfunktion vi.setSystemTime
verwenden. Dieser Wert wird nicht automatisch zwischen verschiedenen Tests zurückgesetzt.
Beachte, dass die Verwendung von vi.useFakeTimers
ebenfalls die Zeit von Date
ändert.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// Die simulierte Zeit zurücksetzen
vi.useRealTimers();
- Eine globale Variable simulieren
Du kannst eine globale Variable setzen, indem du globalThis
einen Wert zuweist oder den Helfer vi.stubGlobal
verwendest. Bei Verwendung von vi.stubGlobal
wird dieser Wert nicht automatisch zwischen Tests zurückgesetzt, es sei denn, du aktivierst die Konfigurationsoption unstubGlobals
oder rufst vi.unstubAllGlobals
auf.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
import.meta.env
simulieren
Um eine Umgebungsvariable zu setzen, kannst du ihr einfach einen neuen Wert zuweisen. Dieser Wert wird nicht automatisch zwischen verschiedenen Tests zurückgesetzt.
import { beforeEach, expect, it } from 'vitest';
// Es kann im beforeEach-Hook manuell zurückgesetzt werden
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');
});
Wenn du den Wert automatisch zurücksetzen möchtest, kannst du den Helfer vi.stubEnv
verwenden, wobei die Konfigurationsoption unstubEnvs
aktiviert ist (oder vi.unstubAllEnvs
manuell im beforeEach
-Hook aufrufen):
import { expect, it, vi } from 'vitest';
// vor dem Ausführen von Tests ist "VITE_ENV" "test"
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('the value is restored before running an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};