Kontekst Testu
Zainspirowany przez Playwright Fixtures, kontekst testowy Vitest umożliwia definiowanie narzędzi, stanów i tzw. "fixtures", które mogą być wykorzystywane w testach.
Użycie
Pierwszym argumentem każdej funkcji zwrotnej testu jest kontekst testu.
import { it } from 'vitest';
it('should work', ({ task }) => {
// wyświetla nazwę testu
console.log(task.name);
});
Wbudowany Kontekst Testu
task
Obiekt tylko do odczytu, zawierający metadane dotyczące testu.
expect
API expect
, które jest powiązane z bieżącym testem:
import { it } from 'vitest';
it('math is easy', ({ expect }) => {
expect(2 + 2).toBe(4);
});
To API jest przydatne do równoległego uruchamiania testów snapshotowych, ponieważ globalny obiekt expect
nie może ich monitorować:
import { it } from 'vitest';
it.concurrent('math is easy', ({ expect }) => {
expect(2 + 2).toMatchInlineSnapshot();
});
it.concurrent('math is hard', ({ expect }) => {
expect(2 * 2).toMatchInlineSnapshot();
});
skip
function skip(note?: string): never;
function skip(condition: boolean, note?: string): void;
Pomija dalsze wykonanie testu i oznacza go jako pominięty:
import { expect, it } from 'vitest';
it('math is hard', ({ skip }) => {
skip();
expect(2 + 2).toBe(5);
});
Od Vitest 3.1 akceptuje parametr typu boolean, aby warunkowo pominąć test:
it('math is hard', ({ skip, mind }) => {
skip(mind === 'foggy');
expect(2 + 2).toBe(5);
});
annotate
3.2.0+
function annotate(
message: string,
attachment?: TestAttachment
): Promise<TestAnnotation>;
function annotate(
message: string,
type?: string,
attachment?: TestAttachment
): Promise<TestAnnotation>;
Dodaje adnotację testową, która zostanie wyświetlona przez reporter.
test('annotations API', async ({ annotate }) => {
await annotate('https://github.com/vitest-dev/vitest/pull/7953', 'issues');
});
signal
3.2.0+
AbortSignal
, który może zostać przerwany przez Vitest. Sygnał jest anulowany w następujących sytuacjach:
- Upływa limit czasu testu
- Użytkownik ręcznie anulował test za pomocą Ctrl+C
vitest.cancelCurrentRun
zostało wywołane programowo- Inny test zakończył się równolegle niepowodzeniem i flaga
bail
jest ustawiona
it('stop request when test times out', async ({ signal }) => {
await fetch('/resource', { signal });
}, 2000);
onTestFailed
Hook onTestFailed
powiązany z bieżącym testem. To API jest przydatne, gdy uruchamiasz testy równolegle i potrzebujesz specjalnego traktowania tylko dla tego konkretnego testu.
onTestFinished
Hook onTestFinished
powiązany z bieżącym testem. To API jest przydatne, gdy uruchamiasz testy równolegle i potrzebujesz specjalnego traktowania tylko dla tego konkretnego testu.
Rozszerzanie Kontekstu Testu
Vitest zapewnia dwa różne sposoby rozszerzania kontekstu testu.
test.extend
Analogicznie do Playwright, możesz użyć tej metody do zdefiniowania własnego API test
z niestandardowymi "fixtures" i ponownego użycia go w dowolnym miejscu.
Na przykład, najpierw definiujemy API test
z dwoma "fixtures": todos
i archive
.
import { test as baseTest } from 'vitest';
const todos = [];
const archive = [];
export const test = baseTest.extend({
todos: async ({}, use) => {
// konfiguracja fixture przed każdym testem
todos.push(1, 2, 3);
// użycie wartości fixture
await use(todos);
// czyszczenie fixture po każdym teście
todos.length = 0;
},
archive,
});
Następnie możemy go zaimportować i użyć.
import { expect } from 'vitest';
import { test } from './my-test.js';
test('add items to todos', ({ todos }) => {
expect(todos.length).toBe(3);
todos.push(4);
expect(todos.length).toBe(4);
});
test('move items from todos to archive', ({ todos, archive }) => {
expect(todos.length).toBe(3);
expect(archive.length).toBe(0);
archive.push(todos.pop());
expect(todos.length).toBe(2);
expect(archive.length).toBe(1);
});
Możemy również dodać więcej "fixtures" lub nadpisać istniejące "fixtures" poprzez rozszerzenie naszego test
.
import { test as todosTest } from './my-test.js';
export const test = todosTest.extend({
settings: {
// ...
},
});
Inicjalizacja Fixture
Moduł uruchamiający Vitest inteligentnie zainicjuje "fixtures" i wstrzyknie je do kontekstu testu w oparciu o ich użycie.
import { test as baseTest } from 'vitest';
const test = baseTest.extend<{
todos: number[];
archive: number[];
}>({
todos: async ({ task }, use) => {
await use([1, 2, 3]);
},
archive: [],
});
// todos nie zostanie uruchomione
test('skip', () => {});
test('skip', ({ archive }) => {});
// todos zostanie uruchomione
test('run', ({ todos }) => {});
WARNING
Podczas używania test.extend()
z "fixtures", zawsze należy używać wzorca dekonstrukcji obiektu { todos }
do dostępu do kontekstu zarówno w funkcji "fixture", jak i w funkcji testu.
test('context must be destructured', (context) => {
expect(context.todos.length).toBe(2)
})
test('context must be destructured', ({ todos }) => {
expect(todos.length).toBe(2)
})
Automatyczna Fixture
Vitest obsługuje również składnię tupli dla "fixtures", pozwalając na przekazywanie opcji dla każdej "fixture". Na przykład, możesz jej użyć do jawnego inicjowania "fixture", nawet jeśli nie jest ona używana w testach.
import { test as base } from 'vitest';
const test = base.extend({
fixture: [
async ({}, use) => {
// ta funkcja zostanie uruchomiona
setup();
await use();
teardown();
},
{ auto: true }, // Oznacz jako automatyczną fixture
],
});
test('works correctly');
Domyślna Fixture
Od Vitest 3, możesz zapewnić różne wartości w różnych projektach. Aby włączyć tę funkcję, przekaż { injected: true }
w opcjach. Jeśli klucz nie jest określony w konfiguracji projektu, zostanie użyta wartość domyślna.
import { test as base } from 'vitest';
const test = base.extend({
url: [
// wartość domyślna, jeśli "url" nie zostało zdefiniowane w konfiguracji
'/default',
// oznacz fixture jako injected, aby umożliwić nadpisanie
{ injected: true },
],
});
test('works correctly', ({ url }) => {
// url to "/default" w "project-new"
// url to "/full" w "project-full"
// url to "/empty" w "project-empty"
});
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
projects: [
{
test: {
name: 'project-new',
},
},
{
test: {
name: 'project-full',
provide: {
url: '/full',
},
},
},
{
test: {
name: 'project-empty',
provide: {
url: '/empty',
},
},
},
],
},
});
Zakres Wartości dla Suite 3.1.0+
Od Vitest 3.1, możesz nadpisywać wartości kontekstu dla każdego zestawu testów (suite) i jego dzieci, używając API test.scoped
:
import { test as baseTest, describe, expect } from 'vitest';
const test = baseTest.extend({
dependency: 'default',
dependant: ({ dependency }, use) => use({ dependency }),
});
describe('use scoped values', () => {
test.scoped({ dependency: 'new' });
test('uses scoped value', ({ dependant }) => {
// `dependant` używa nowej nadpisanej wartości, która dotyczy
// wszystkich testów w tym suite
expect(dependant).toEqual({ dependency: 'new' });
});
describe('keeps using scoped value', () => {
test('uses scoped value', ({ dependant }) => {
// zagnieżdżony suite odziedziczył wartość
expect(dependant).toEqual({ dependency: 'new' });
});
});
});
test('keep using the default values', ({ dependant }) => {
// `dependency` używa wartości domyślnej
// poza suite z .scoped
expect(dependant).toEqual({ dependency: 'default' });
});
To API jest szczególnie przydatne, jeśli masz wartość kontekstu, która zależy od zmiennej dynamicznej, takiej jak połączenie z bazą danych:
const test = baseTest.extend<{
db: Database;
schema: string;
}>({
db: async ({ schema }, use) => {
const db = await createDb({ schema });
await use(db);
await cleanup(db);
},
schema: '',
});
describe('one type of schema', () => {
test.scoped({ schema: 'schema-1' });
// ... testy
});
describe('another type of schema', () => {
test.scoped({ schema: 'schema-2' });
// ... testy
});
Kontekst dla poszczególnych zakresów 3.2.0+
Możesz zdefiniować kontekst, który zostanie zainicjowany raz na plik lub na worker. Jest on inicjowany w ten sam sposób, co zwykła "fixture", przyjmując obiekt jako parametr:
import { test as baseTest } from 'vitest';
export const test = baseTest.extend({
perFile: [({}, { use }) => use([]), { scope: 'file' }],
perWorker: [({}, { use }) => use([]), { scope: 'worker' }],
});
Wartość jest inicjowana za pierwszym razem, gdy jakikolwiek test ma do niej dostęp, chyba że opcje "fixture" mają auto: true
- w tym przypadku wartość jest inicjowana przed uruchomieniem jakiegokolwiek testu.
const test = baseTest.extend({
perFile: [
({}, { use }) => use([]),
{
scope: 'file',
// zawsze uruchamiany ten hook przed jakimkolwiek testem
auto: true,
},
],
});
Zakres worker
uruchomi "fixture" raz na worker. Liczba uruchomionych workerów zależy od różnych czynników. Domyślnie każdy plik działa w osobnym workerze, więc zakresy file
i worker
działają tak samo.
Jednakże, jeśli izolacja zostanie wyłączona (izolację), to liczba workerów jest ograniczona przez konfigurację maxWorkers
lub poolOptions
.
Zauważ, że określenie scope: 'worker'
podczas uruchamiania testów w vmThreads
lub vmForks
będzie działać tak samo jak scope: 'file'
. To ograniczenie istnieje, ponieważ każdy plik testowy ma swój własny kontekst VM, więc gdyby Vitest zainicjował go jednokrotnie, jeden kontekst mógłby przeniknąć do drugiego i stworzyć wiele niespójności referencyjnych (instancje tej samej klasy odwoływałyby się do różnych konstruktorów, na przykład).
TypeScript
Aby zapewnić typy "fixture" dla wszystkich niestandardowych kontekstów, możesz przekazać typ "fixtures" jako typ generyczny.
interface MyFixtures {
todos: number[];
archive: number[];
}
const test = baseTest.extend<MyFixtures>({
todos: [],
archive: [],
});
test('types are defined correctly', ({ todos, archive }) => {
expectTypeOf(todos).toEqualTypeOf<number[]>();
expectTypeOf(archive).toEqualTypeOf<number[]>();
});
Wnioskowanie typów
Zauważ, że Vitest nie obsługuje wnioskowania typów, gdy wywoływana jest funkcja use
. Zawsze preferowane jest przekazywanie całego typu kontekstu jako typu generycznego, gdy wywoływana jest test.extend
:
import { test as baseTest } from 'vitest';
const test = baseTest.extend<{
todos: number[];
schema: string;
}>({
todos: ({ schema }, use) => use([]),
schema: 'test',
});
test('types are correct', ({
todos, // number[]
schema, // string
}) => {
// ...
});
beforeEach
i afterEach
Przestarzałe
To przestarzały sposób rozszerzania kontekstu i nie będzie działać, gdy test
zostanie rozszerzony za pomocą test.extend
.
Konteksty są różne dla każdego testu. Można uzyskać do nich dostęp i rozszerzyć je w hookach beforeEach
i afterEach
.
import { beforeEach, it } from 'vitest';
beforeEach(async context => {
// rozszerz kontekst
context.foo = 'bar';
});
it('should work', ({ foo }) => {
console.log(foo); // 'bar'
});
TypeScript
Aby zapewnić typy właściwości dla wszystkich niestandardowych kontekstów, można rozszerzyć typ TestContext
dodając:
declare module 'vitest' {
export interface TestContext {
foo?: string;
}
}
Jeśli typy właściwości mają być zapewnione tylko dla konkretnych hooków beforeEach
, afterEach
, it
i test
, można przekazać typ jako typ generyczny.
interface LocalTestContext {
foo: string;
}
beforeEach<LocalTestContext>(async context => {
// typeof context to 'TestContext & LocalTestContext'
context.foo = 'bar';
});
it<LocalTestContext>('should work', ({ foo }) => {
// typeof foo to 'string'
console.log(foo); // 'bar'
});