Skip to content

每个测试使用数据库事务

接触真实数据库的集成测试需要从干净状态开始。在每个测试之间截断表很慢,所以常见的变通方法是把每个测试包裹在一个事务中,并在测试结束时回滚。不会发生任何提交,也不需要编写逐个测试的清理代码。

Vitest 通过 aroundEach 4.1.0+ 作用域 fixture 3.2.0+ 提供了这一能力。

模式

ts
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 进程中共享一个连接:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    isolate: false,
  },
})
ts
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 里的模块状态。在整个套件范围内关闭隔离之前,请先阅读 按文件隔离设置 这一配方中的权衡。

vmThreadsvmForks 无论 isolate 标志如何都始终在隔离环境中运行,因此在这些池中,worker 作用域的 fixture 会退化为按文件行为。

另请参阅