测试上下文
受 Playwright Fixtures 的启发,Vitest 的测试上下文允许您定义可在测试中使用的工具、状态和夹具(fixture)。
用法
每个测试回调函数的第一个参数是测试上下文。
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+
一个 AbortSignal
,可由 Vitest 中止。信号会在以下情况下被中止:
- 测试超时
- 用户使用 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,其中包含自定义夹具,并可在任何地方重用。
例如,我们首先创建一个包含两个夹具(todos
和 archive
)的 test
收集器。
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 还支持夹具的元组语法,允许您为每个夹具传递选项。例如,您可以使用它来显式初始化夹具,即使该夹具未在测试中使用。
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 }) => {
// url 在 "project-new" 中是 "/default"
// url 在 "project-full" 中是 "/full"
// url 在 "project-empty" 中是 "/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'
});