进阶 vitest.dev 2026-10-22 19:00:41 · 16 阅读

第3章 Vitest 测试上下文详解:Fixture、内置 API 与动态控制

测试上下文

受 Playwright Fixtures 启发,Vitest 的测试上下文允许你定义在测试中使用的工具函数、状态和 fixture。

用法

每个测试回调的第一个参数就是测试上下文。

ts import { it } from 'vitest'

it('should work', ({ task }) => { // 打印测试名称 console.log(task.name) })

内置测试上下文

task

一个只读对象,包含关于当前测试的元数据。

expect

绑定到当前测试的 expect API:

ts import { it } from 'vitest'

it('math is easy', ({ expect }) => { expect(2 + 2).toBe(4) })

这个 API 在并发运行快照测试时很有用,因为全局 expect 无法追踪它们:

ts 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

ts function skip(note?: string): never function skip(condition: boolean, note?: string): void

跳过后续的测试执行并将测试标记为已跳过:

ts import { expect, it } from 'vitest'

it('math is hard', ({ skip }) => { skip() expect(2 + 2).toBe(5) })

从 Vitest 3.1 开始,它接受一个布尔参数来条件性地跳过测试:

ts it('math is hard', ({ skip, mind }) => { skip(mind === 'foggy') expect(2 + 2).toBe(5) })

annotate 3.2.0+

ts function annotate( message: string, attachment?: TestAttachment, ): Promise

function annotate( message: string, type?: string, attachment?: TestAttachment, ): Promise

添加测试注释,这些注释将由你的 reporter 显示。

ts 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 标志

ts it('stop request when test times out', async ({ signal }) => { await fetch('/resource', { signal }) }, 2000)

bench 5.0.0+

bench fixture 允许你在常规测试中定义和运行基准测试。你可以测量吞吐量、比较实现方案并断言相对性能:

ts import { expect, test } from 'vitest'

test('compare parsers', async ({ bench }) => { const result = await bench.compare( bench('JSON.parse', () => { JSON.parse('{"key":"value"}') }), bench('custom parser', () => { customParse('{"key":"value"}') }), )

expect(result.get('JSON.parse')).toBeFasterThan(result.get('custom parser')) })

有关比较、基线和断言匹配器的完整文档,请参阅 Benchmarks 指南。

onTestFailed

绑定到当前测试的 onTestFailed 钩子。当你并发运行测试且仅需要对此特定测试进行特殊处理时,此 API 很有用。

onTestFinished

绑定到当前测试的 onTestFinished 钩子。当你并发运行测试且仅需要对此特定测试进行特殊处理时,此 API 很有用。

扩展测试上下文

Vitest 允许使用 test.extend 通过自定义 fixture 扩展测试上下文。

test.extend 方法允许你使用 fixture 创建自定义测试 API——fixture 是可用的值,会自动为你的测试进行设置和拆卸。Vitest 支持两种语法:构建器模式(推荐)和对象语法(兼容 Playwright)。

构建器模式 4.1.0+

构建器模式是定义 fixture 的推荐方式,因为它提供自动类型推断。TypeScript 会根据 fixture 的返回值推断其类型,因此你不需要手动声明类型。

my-test.ts ts import { test as baseTest } from 'vitest'

export const test = baseTest // 简单值 - 类型被推断为 { port: number; host: string } .extend('config', { port: 3000, host: 'localhost' }) // 函数 fixture - 类型从返回值推断 .extend('server', async ({ config }) => { // TypeScript 知道 config 是 { port: number; host: string } return `http://${config.host}:${config.port}` })

然后在测试中使用它:

my-test.test.ts ts import { expect } from 'vitest' import { test } from './my-test.js'

test('server uses correct port', ({ config, server }) => { // TypeScript 知道类型: // - config 是 { port: number; host: string } // - server 是 string expect(server).toBe('http://localhost:3000') expect(config.port).toBe(3000) })

使用 onCleanup 进行设置和清理

对于需要设置或清理逻辑的 fixture,请使用函数。onCleanup 回调会注册拆卸逻辑,在 fixture 的作用域结束后运行:

ts import { test as baseTest } from 'vitest'

export const test = baseTest .extend('tempFile', async ({}, { onCleanup }) => { const filePath = `/tmp/test-${Date.now()}.txt` await fs.writeFile(filePath, 'test data')

// 注册清理 - 测试完成后运行 onCleanup(async () => { await fs.unlink(filePath) })

return filePath })

更复杂的示例:

ts const test = baseTest .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => { const db = await createDatabase() await db.connect()

onCleanup(async () => { await db.disconnect() })

return db }) .extend('user', async ({ database }, { onCleanup }) => { const user = await database.createTestUser()

onCleanup(async () => { await database.deleteUser(user.id) })

return user })

警告 每个 fixture 只能调用一次 onCleanup 函数。如果你需要多个清理操作,要么将它们合并到一个清理函数中,要么将你的 fixture 拆分为多个较小的 fixture:

ts // ❌ 这会抛出错误 const test = baseTest .extend('resources', async ({}, { onCleanup }) => { const a = await acquireA() onCleanup(() => releaseA(a))

const b = await acquireB() onCleanup(() => releaseB(b)) // 错误:onCleanup 只能被调用一次

return { a, b } })

// ✅ 拆分为单独的 fixture(推荐) const test = baseTest .extend('resourceA', async ({}, { onCleanup }) => { const a = await acquireA() onCleanup(() => releaseA(a)) return a }) .extend('resourceB', async ({}, { onCleanup }) => { const b = await acquireB() onCleanup(() => releaseB(b)) return b })

拆分为单独的 fixture 是推荐的方法,因为它提供了更好的隔离性并明确了依赖关系。

Fixture 选项

.extend() 的第二个参数接受选项:

ts const test = baseTest // 自动 fixture - 即使未使用也会为每个测试运行 .extend('metrics', { auto: true }, ({}, { onCleanup }) => { const metrics = new MetricsCollector() metrics.start() onCleanup(() => metrics.stop()) return metrics }) // Worker 作用域 fixture - 每个 worker 初始化一次 .extend('config', { scope: 'worker' }, () => { return loadConfig() }) // 文件作用域 fixture - 每个文件初始化一次 .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => { const db = await createDatabase(config) onCleanup(() => db.close()) return db }) // 注入 fixture - 可以通过配置覆盖 .extend('baseUrl', { injected: true }, () => { return 'http://localhost:3000' })

对于测试作用域的 fixture(默认值),你可以省略选项:

ts const test = baseTest .extend('simple', () => 'value')

访问其他 Fixture

每个 fixture 可以通过其第一个参数访问之前定义的 fixture。这对于函数和非函数 fixture 都有效:

ts const test = baseTest .extend('config', { apiUrl: 'https://api.example.com', port: 3000 }) .extend('client', ({ config }) => { // TypeScript 知道 config 是 { apiUrl: string; port: number } return new ApiClient(config.apiUrl) }) .extend('user', async ({ client }) => { // TypeScript 知道 client 是 ApiClient return await client.getCurrentUser() })

对象语法(兼容 Playwright)

Vitest 还支持兼容 Playwright 的对象语法。如果你正在从 Playwright 迁移或喜欢一次性定义所有 fixture,这很有用:

my-test.ts ts import { test as baseTest } from 'vitest'

export const test = baseTest.extend({ page: async ({}, use) => { // 在每个测试函数之前设置 fixture const page = await browser.newPage()

// 使用 fixture 值 await use(page)

// 在每个测试函数之后清理 fixture await page.close() }, baseUrl: 'http://localhost:3000' })

与构建器模式的关键区别在于使用 use() 回调模式进行清理:

ts // 对象语法:清理代码放在 use() 之后 const test = baseTest.extend({ database: async ({}, use) => { const db = await createDatabase() await db.connect()

await use(db) // 测试在此处运行

// 测试后清理 await db.disconnect() } })

// 构建器模式:清理通过 onCleanup() 注册 const test = baseTest .extend('database', async ({}, { onCleanup }) => { const db = await createDatabase() await db.connect()

onCleanup(() => db.disconnect())

return db // 测试在此返回后运行 })

信息 使用对象语法时,由于 TypeScript 无法从 use() 回调中推断类型,你需要手动作为泛型参数提供类型:

ts const test = baseTest.extend<{ page: Page baseUrl: string }>({ page: async ({}, use) => { const page = await browser.newPage() await use(page) await page.close() }, baseUrl: 'http://localhost:3000' })

用于选项的元组语法

使用对象语法时,使用元组来指定 fixture 选项:

ts const test = baseTest.extend({ // 自动 fixture fixture: [ async ({}, use) => { setup() await use() teardown() }, { auto: true } ], // 作用域 fixture database: [ async ({}, use) => { const db = await createDatabase() await use(db) await db.close() }, { scope: 'file' } ], // 注入 fixture url: [ '/default', { injected: true } ], })

Fixture 初始化

Vitest runner 会根据使用情况智能地初始化你的 fixture 并注入到测试上下文中。

ts import { test as baseTest } from 'vitest'

const test = baseTest .extend('database', async () => { console.log('database initializing') return createDatabase() }) .extend('cache', async () => { return createCache() })

// database 不会运行 test('no fixtures needed', () => {}) test('only cache', ({ cache }) => {})

// database 会运行 test('needs database', ({ database }) => {})

警告 在使用 test.extend() 和 fixture 时,你应该始终使用对象解构模式 { database } 在 fixture 函数和测试函数中访问上下文。

ts test('context must be destructured', (context) => { expect(context.database).toBeDefined() })

test('context must be destructured', ({ database }) => { expect(database).toBeDefined() })

扩展已扩展的测试

你可以扩展一个已经扩展过的测试以添加更多 fixture:

ts import { test as dbTest } from './my-test.js'

export const test = dbTest .extend('user', ({ database }) => { return database.createUser() })

使用对象语法:

ts import { test as dbTest } from './my-test.js'

export const test = dbTest.extend({ admin: async ({ database }, use) => { const admin = await database.createAdmin() await use(admin) await database.deleteUser(admin.id) } })

混合两种语法

你可以结合这两种方法。构建器模式可以在基于对象的扩展之后进行链式调用:

ts const test = baseTest // 对象语法用于简单 fixture .extend<{ apiKey: string }>({ apiKey: 'test-key-123', }) // 构建器模式用于带推断的复杂 fixture .extend('client', ({ apiKey }) => { // TypeScript 知道 apiKey 是 string return new ApiClient(apiKey) })

Fixture 作用域 3.2.0+

默认情况下,fixture 为每个测试初始化。你可以使用 scope 选项来在测试之间共享 fixture。

警告 默认情况下,没有 scope 的任何 fixture 都被视为测试 fixture。这意味着你不能在 worker 和 file 作用域内使用它。如果你希望在那里访问它,请考虑手动指定 scope:

ts test .extend('port', { scope: 'worker' }, 5000) .extend('db', { scope: 'worker' }, async ({ port }) => { return createDb(port) })

注意,你不能在 describe 块内覆盖非测试 fixture:

ts test.describe('a nested suite', () => { test.override('port', { scope: 'worker' }, 3000) // 抛出错误 })

考虑在模块顶层覆盖它,或者使用 injected 选项并在项目配置中提供值。

还要注意,在非 isolate 模式下,覆盖 worker fixture 会影响在其后被覆盖后运行的所有测试文件中的 fixture 值。

测试作用域(默认)

测试作用域的 fixture 会为每个测试创建一个新的实例:

ts const test = baseTest .extend('counter', () => { return { value: 0 } })

test('first test', ({ counter }) => { counter.value++ expect(counter.value).toBe(1) })

test('second test', ({ counter }) => { // 新实例,值再次为 0 expect(counter.value).toBe(0) })

测试作用域的 fixture 可以访问内置测试上下文(task、expect、skip 等):

ts const test = baseTest .extend('testInfo', ({ task }) => { return { name: task.name } })

文件作用域

文件作用域的 fixture 在每个测试文件中初始化一次:

ts const test = baseTest .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => { const db = await createDatabase() onCleanup(() => db.close()) return db })

test('first test', ({ database }) => { // 使用相同的 database 实例 })

test('second test', ({ database }) => { // 与第一个测试相同的 database 实例 })

Worker 作用域

Worker 作用域的 fixture 在每个 worker 进程中初始化一次:

ts const test = baseTest .extend('config', { scope: 'worker' }, () => { return await loadExpensiveConfig() })

信息 默认情况下,每个文件都在单独的 worker 中运行,因此 file 和 worker 作用域的工作方式相同。然而,如果你禁用隔离,那么 worker 的数量受 maxWorkers 限制,并且 worker 作用域的 fixture 将在同一 worker 中运行的文件之间共享。

当在 vmThreads 或 vmForks 中运行测试时,scope: 'worker' 的工作方式与 scope: 'file' 相同,因为每个文件都有自己的 VM 上下文。

作用域层级

Fixture 只能访问相同或更高(生命周期更长)作用域的其他 fixture:

Fixture 作用域 | 可访问 worker | 仅限其他 worker fixtures file | Worker + file fixtures test | Worker + file + test fixtures + 测试上下文

ts const test = baseTest .extend('config', { scope: 'worker' }, () => { return { apiUrl: 'https://api.example.com' } }) .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => { // ✅ File fixture 可以访问 worker fixture const db = await createDatabase(config.apiUrl) onCleanup(() => db.close()) return db }) .extend('user', async ({ database, task }) => { // ✅ Test fixture 可以访问 file fixture 和测试上下文 return await database.createUser(task.name) })

提示 只有测试作用域的 fixture 可以访问内置测试上下文(task、expect、skip 等)。Worker 和 file fixture 在任何特定测试之外运行,因此无法访问特定的测试属性。

如果你需要在文件作用域的 fixture 中获取文件路径,请使用 expect.getState().testPath。

类型安全的作用域访问 3.2.0+

使用构建器模式时,TypeScript 会自动执行基于作用域的访问规则。如果你试图从文件作用域 fixture 访问测试作用域 fixture,你会得到编译时错误。

如果你使用对象语法并希望获得相同的类型安全性,可以使用 $worker、$file 和 $test 键来显式声明哪些 fixture 属于哪个作用域:

ts const test = baseTest.extend<{ $worker: { config: Config } $file: { database: Database } $test: { user: User } }>({ config: [async ({}, use) => { await use(loadConfig()) }, { scope: 'worker' }],

database: [async ({ config }, use) => { const db = await createDatabase(config) await use(db) await db.close() }, { scope: 'file' }],

user: async ({ database }, use) => { const user = await database.createUser() await use(user) await database.deleteUser(user.id) }, })

这提供了与构建器模式相同的编译时安全性,在构建时而非运行时捕获作用域违规。

默认 Fixture(注入)

从 Vitest 3 开始,你可以为不同的项目提供不同的值。为此,请在选项中传递 { injected: true }。如果项目配置中未指定该键,则使用默认值。

fixtures.test.ts ts import { test as baseTest } from 'vitest'

const test = baseTest .extend('url', { injected: true }, '/default')

test('works correctly', ({ url }) => { // url 在 "project-new" 中是 "/default" // url 在 "project-full" 中是 "/full" // url 在 "project-empty" 中是 "/empty" })

vitest.config.ts ts 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', }, }, }, ], }, })

覆盖 Fixture 值 4.1.0+

你可以使用 test.override 为特定套件及其子级覆盖 fixture 值。当你需要为不同的测试场景提供不同的 fixture 值时,这很有用。

提示 Vitest 会自动继承选项,如果在覆盖时未提供它们。请注意,你不能覆盖 fixture 的 scope 或 auto 选项。

构建器模式(推荐)

ts import { test as baseTest, describe, expect } from 'vitest'

const test = baseTest .extend('config', { port: 3000, host: 'localhost' }) .extend('server', ({ config }) => `http://${config.host}:${config.port}`)

describe('production environment', () => { // 使用新的静态值覆盖(可链接) test .override('config', { port: 8080, host: 'api.example.com' })

test('uses production config', ({ server }) => { expect(server).toBe('http://api.example.com:8080') }) })

describe('with custom server', () => { // 使用可以访问其他 fixture 的函数覆盖 test.override('server', ({ config }) => { return `https://${config.host}:${config.port}/v2` })

test('uses custom server', ({ server }) => { expect(server).toBe('https://localhost:3000/v2') }) })

test('uses default values', ({ server }) => { expect(server).toBe('http://localhost:3000') })

链接多个覆盖

test.override 返回 test API,因此你可以链接多个调用:

ts describe('production environment', () => { test .override('environment', 'production') .override('port', 8080) .override('debug', false)

test('uses production settings', ({ environment, port, debug }) => { expect(environment).toBe('production') expect(port).toBe(8080) expect(debug).toBe(false) }) })

对象语法

你也可以使用对象语法一次覆盖多个 fixture:

ts describe('different configuration', () => { test.override({ config: { port: 4000, host: 'test.local' }, })

test('uses overwritten config', ({ config }) => { expect(config.port).toBe(4000) }) })

带清理

使用函数覆盖时,你可以像 test.extend 一样使用 onCleanup:

ts describe('with custom database', () => { test.override('database', async ({ config }, { onCleanup }) => { const db = await createTestDatabase(config) onCleanup(() => db.drop()) return db })

test('uses custom database', ({ database }) => { // 使用覆盖的 database }) })

嵌套作用域

覆盖会被嵌套套件继承,并且可以再次被覆盖:

ts describe('level 1', () => { test.override('value', 'one')

test('uses level 1 value', ({ value }) => { expect(value).toBe('one') })

describe('level 2', () => { test.override('value', 'two')

test('uses level 2 value', ({ value }) => { expect(value).toBe('two') }) })

test('still uses level 1 value', ({ value }) => { expect(value).toBe('one') }) })

警告 注意,你不能在 test.override 中引入新的 fixture。请改用 test.extend 来扩展测试上下文。

信息 test.scoped 已被弃用,推荐使用 test.override。test.scoped API 仍然有效,但将在未来版本中移除。

类型安全的钩子

使用 test.extend 时,扩展后的 test 对象提供知道扩展上下文的类型安全钩子:

ts const test = baseTest .extend('counter', { value: 0, increment() { this.value++ } })

// 与全局钩子不同,这些钩子知道扩展的上下文 test.beforeEach(({ counter }) => { counter.increment() })

test.afterEach(({ counter }) => { console.log('Final count:', counter.value) })

套件级别钩子与 Fixtures 4.1.0+

扩展后的 test 对象还提供 beforeAll、afterAll 和 aroundAll 钩子,它们可以访问文件作用域和 worker 作用域的 fixture:

ts const test = baseTest .extend('config', { scope: 'file' }, () => loadConfig()) .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => { const db = await createDatabase(config) onCleanup(() => db.close()) return db })

// 在套件级别钩子中访问文件作用域 fixture test.aroundAll(async (runSuite, { database }) => { await database.transaction(runSuite) })

test.beforeAll(async ({ database }) => { await database.createUsers() })

test.afterAll(async ({ database }) => { await database.removeUsers() })

重要 套件级别钩子(beforeAll、afterAll、aroundAll)必须在 test.extend() 返回的 test 对象上调用,才能访问扩展的 fixture。使用全局 beforeAll/afterAll/aroundAll 函数将无法访问你的自定义 fixture:

ts import { test as baseTest, beforeAll } from 'vitest'

const test = baseTest .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => { const db = await createDatabase() onCleanup(() => db.close()) return db })

// ❌ 错误:全局 beforeAll 无法访问 'database' beforeAll(({ database }) => { // 错误:'database' 未定义 })

// ✅ 正确:使用 test.beforeAll 来访问 fixture test.beforeAll(({ database }) => { // 'database' 可用 })

这适用于所有套件级别钩子:beforeAll、afterAll 和 aroundAll。

提示 套件级别钩子只能访问文件作用域和 worker 作用域的 fixture,包括自动 fixture。测试作用域的 fixture 在这些钩子中不可用,因为它们在没有单个测试上下文的情况下运行。如果你试图在套件级别钩子中访问测试作用域的 fixture,Vitest 将抛出错误。

ts const test = baseTest .extend('testFixture', () => 'test-scoped') .extend('fileFixture', { scope: 'file' }, () => 'file-scoped')

// ❌ 错误:在 beforeAll 中无法访问测试作用域 fixture test.beforeAll(({ testFixture }) => {})

// ✅ 有效:文件作用域 fixture 可用 test.beforeAll(({ fileFixture }) => {})

评论 (0)