測試上下文
受 Playwright Fixtures 啟發,Vitest 的測試上下文允許您定義可在測試中使用的輔助函數、狀態及夾具(fixtures)。
用法
每個測試回呼函數的第一個參數都是測試上下文。
import { it } from 'vitest';
it('should work', ({ task }) => {
// 輸出測試名稱
console.log(task.name);
});
內建測試上下文
task
一個唯讀物件,包含關於測試的元資料。
expect
綁定到當前測試的 expect
API:
import { it } from 'vitest';
it('math is easy', ({ expect }) => {
expect(2 + 2).toBe(4);
});
此 API 對於並行執行快照測試非常有用,因為全域的 expect
無法追蹤它們:
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;
跳過後續測試執行並將測試標記為已跳過:
import { expect, it } from 'vitest';
it('math is hard', ({ skip }) => {
skip();
expect(2 + 2).toBe(5);
});
自 Vitest 3.1 起,它接受一個布林值參數,以便根據條件跳過測試:
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>;
test('annotations API', async ({ annotate }) => {
await annotate('https://github.com/vitest-dev/vitest/pull/7953', 'issues');
});
signal
3.2.0+
一個可由 Vitest 中止的 AbortSignal
。在以下情況下,信號會中止:
- 測試逾時
- 使用者手動按下 Ctrl+C 取消測試執行
vitest.cancelCurrentRun
透過程式碼呼叫- 另一個測試在並行執行時失敗且已設定
bail
旗標
it('stop request when test times out', async ({ signal }) => {
await fetch('/resource', { signal });
}, 2000);
onTestFailed
綁定到當前測試的 onTestFailed
鉤子。如果您並行執行測試且僅需針對此特定測試進行特殊處理,則此 API 很有用。
onTestFinished
綁定到當前測試的 onTestFinished
鉤子。如果您並行執行測試且僅需針對此特定測試進行特殊處理,則此 API 很有用。
擴展測試上下文
Vitest 提供兩種擴展測試上下文的方式。
test.extend
與 Playwright 類似,您可以使用此方法定義自己的 test
API,其中包含自訂夾具並在任何地方重複使用。
例如,我們首先建立一個包含兩個夾具的 test
收集器:todos
和 archive
。
import { test as baseTest } from 'vitest';
const todos = [];
const archive = [];
export const test = baseTest.extend({
todos: async ({}, use) => {
// 在每個測試函數之前設定夾具
todos.push(1, 2, 3);
// 使用夾具值
await use(todos);
// 在每個測試函數之後清理夾具
todos.length = 0;
},
archive,
});
然後我們可以導入並使用它。
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);
});
我們還可以透過擴展我們的 test
來添加更多夾具或覆寫現有夾具。
import { test as todosTest } from './my-test.js';
export const test = todosTest.extend({
settings: {
// ...
},
});
夾具初始化
Vitest 執行器將根據使用情況智能地初始化您的夾具並將它們注入測試上下文。
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 將不會執行
test('skip', () => {});
test('skip', ({ archive }) => {});
// todos 將會執行
test('run', ({ todos }) => {});
WARNING
當使用 test.extend()
搭配夾具時,您應該始終使用物件解構模式 { todos }
來存取夾具函數和測試函數中的上下文。
test('context must be destructured', (context) => {
expect(context.todos.length).toBe(2)
})
test('context must be destructured', ({ todos }) => {
expect(todos.length).toBe(2)
})
自動夾具
Vitest 也支援夾具的元組(tuple)語法,允許您為每個夾具傳遞選項。例如,您可以使用它來顯式初始化夾具,即使它未在測試中使用。
import { test as base } from 'vitest';
const test = base.extend({
fixture: [
async ({}, use) => {
// 此函數將會執行
setup();
await use();
teardown();
},
{ auto: true }, // 標記為自動夾具
],
});
test('works correctly');
預設夾具
自 Vitest 3 起,您可以在不同的專案中設置不同的值。要啟用此功能,請將 { injected: true }
傳遞至選項。如果專案配置中未指定該鍵,則將使用預設值。
import { test as base } from 'vitest';
const test = base.extend({
url: [
// 如果配置中未定義 "url" 的預設值
'/default',
// 將夾具標記為 "injected" 以允許覆寫
{ injected: true },
],
});
test('works correctly', ({ url }) => {
// 在 "project-new" 中 url 為 "/default"
// 在 "project-full" 中 url 為 "/full"
// 在 "project-empty" 中 url 為 "/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',
},
},
},
],
},
});
將值作用範圍到套件 3.1.0+
自 Vitest 3.1 起,您可以使用 test.scoped
API 覆寫每個套件及其子級的上下文值:
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 使用新的覆寫值,該值作用範圍於
// 此套件中的所有測試
expect(dependant).toEqual({ dependency: 'new' });
});
describe('keeps using scoped value', () => {
test('uses scoped value', ({ dependant }) => {
// 巢狀套件繼承了該值
expect(dependant).toEqual({ dependency: 'new' });
});
});
});
test('keep using the default values', ({ dependant }) => {
// dependency 正在使用預設值
// 在 .scoped 套件之外的值
expect(dependant).toEqual({ dependency: 'default' });
});
如果您有一個依賴於動態變數(例如資料庫連線)的上下文值,則此 API 特別有用:
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' });
// ... tests
});
describe('another type of schema', () => {
test.scoped({ schema: 'schema-2' });
// ... tests
});
每作用範圍上下文 3.2.0+
您可以定義每個檔案或每個工作者僅初始化一次的上下文。它的初始化方式與帶有物件參數的常規夾具相同:
import { test as baseTest } from 'vitest';
export const test = baseTest.extend({
perFile: [({}, { use }) => use([]), { scope: 'file' }],
perWorker: [({}, { use }) => use([]), { scope: 'worker' }],
});
除非夾具選項中設定了 auto: true
,否則該值會在任何測試首次存取時初始化——在這種情況下,該值會在任何測試執行之前初始化。
const test = baseTest.extend({
perFile: [
({}, { use }) => use([]),
{
scope: 'file',
// 總是在任何測試之前執行此鉤子
auto: true,
},
],
});
worker
作用範圍將為每個工作者執行夾具一次。正在執行的工作者數量取決於各種因素。預設情況下,每個檔案都在單獨的工作者中執行,因此 file
和 worker
作用範圍的工作方式相同。
但是,如果您禁用隔離,則工作者數量將受 maxWorkers
或 poolOptions
配置的限制。
請注意,在 vmThreads
或 vmForks
中執行測試時,指定 scope: 'worker'
的行為與 scope: 'file'
相同。存在此限制是因為每個測試檔案都有自己的 VM 上下文,因此如果 Vitest 僅初始化一次,一個上下文可能會洩漏到另一個上下文並導致許多參考不一致(例如,相同類別的實例可能會參考不同的建構函式)。
TypeScript
要為所有自訂上下文提供夾具類型,您可以將夾具類型以泛型形式傳遞。
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[]>();
});
類型推論
請注意,Vitest 不支援在呼叫 use
函數時進行類型推論。當呼叫 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
和 afterEach
已棄用
這是一種過時的擴展上下文方式,當 test
使用 test.extend
擴展時,它將不起作用。
每個測試的上下文都不同。您可以在 beforeEach
和 afterEach
鉤子中存取和擴展它們。
import { beforeEach, it } from 'vitest';
beforeEach(async context => {
// 擴展上下文
context.foo = 'bar';
});
it('should work', ({ foo }) => {
console.log(foo); // 'bar'
});
TypeScript
要為所有自訂上下文提供屬性類型,您可以透過添加以下內容來擴充 TestContext
類型:
declare module 'vitest' {
export interface TestContext {
foo?: string;
}
}
如果您只想為特定的 beforeEach
、afterEach
、it
和 test
鉤子提供屬性類型,您可以將類型以泛型形式傳遞。
interface LocalTestContext {
foo: string;
}
beforeEach<LocalTestContext>(async context => {
// context 的類型為 'TestContext & LocalTestContext'
context.foo = 'bar';
});
it<LocalTestContext>('should work', ({ foo }) => {
// foo 的類型為 'string'
console.log(foo); // 'bar'
});