每个测试使用数据库事务
接触真实数据库的集成测试需要从干净状态开始。在每个测试之间截断表很慢,所以常见的变通方法是把每个测试包裹在一个事务中,并在测试结束时回滚。不会发生任何提交,也不需要编写逐个测试的清理代码。
Vitest 通过 aroundEach 4.1.0+ 和 作用域 fixture 3.2.0+ 提供了这一能力。
模式
import { test as baseTest } from 'vitest'
import { createTestDatabase } from './db.ts'
export const test = baseTest
.extend('db', { scope: 'file' }, async ({}, { onCleanup }) => {
const db = await createTestDatabase()
onCleanup(() => db.close())
return db
})
test.aroundEach(async (runTest, { db }) => {
await db.transaction(runTest)
})
test('insert user', async ({ db }) => {
await db.insert({ name: 'Alice' })
// 测试结束时会自动回滚
})工作原理
db fixture 通过 scope: 'file' 每个文件创建一次,因此连接设置只会执行一次,而不是每个测试都执行;文件结束时,onCleanup 会关闭连接。aroundEach 使用 db.transaction(runTest) 包裹每个测试,而测试写入的任何内容都会在 runTest 解析后回滚。测试通过其上下文接收到同一个 db 实例,并且并不知道自己运行在事务中。
只要你的数据库驱动支持嵌套事务或保存点,这种方式就可行,而这涵盖了大多数现代数据库。如果你希望在测试中连同事务一起传递租户 ID 或跟踪 ID 之类的信息,同一个 aroundEach 钩子也可以包裹一个 AsyncLocalStorage 上下文。
每个 worker 一个连接
如果测试套件有很多文件,那么在每个文件上都支付一次全新的数据库连接成本会累积起来。将 fixture 切换为 scope: 'worker' 并关闭隔离,就可以让多个文件在每个 worker 进程中共享一个连接:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
isolate: false,
},
})import { test as baseTest } from 'vitest'
import { createTestDatabase } from './db.ts'
export const test = baseTest
.extend('db', { scope: 'worker' }, async ({}, { onCleanup }) => {
const db = await createTestDatabase()
onCleanup(() => db.close())
return db
})
test.aroundEach(async (runTest, { db }) => {
await db.transaction(runTest)
})默认情况下,每个测试文件都在各自的 worker 中运行,因此 scope: 'file' 和 scope: 'worker' 的行为完全相同。启用 isolate: false 后,Vitest 会在文件之间复用 worker(上限由 maxWorkers 控制),因此 worker 作用域的 fixture 会变成每个 worker 创建一次,而不是每个文件一次。对于一个在 8 个 worker 上运行的 200 文件套件来说,这意味着只需要 8 个连接,而不是 200 个。
复用 worker 并不是免费的优化。关闭隔离后,文件会在 worker 内共享模块实例,而那些会修改顶层状态的测试(计数器、缓存、猴子补丁过的全局对象)可能会把这些状态泄漏给同一个 worker 中下一个运行的文件。每个测试的回滚可以处理数据库中的数据隔离,但无法保护 worker 里的模块状态。在整个套件范围内关闭隔离之前,请先阅读 按文件隔离设置 这一配方中的权衡。
vmThreads 和 vmForks 无论 isolate 标志如何都始终在隔离环境中运行,因此在这些池中,worker 作用域的 fixture 会退化为按文件行为。
