在测试之间重置状态

本指南涵盖在 AdonisJS 测试中管理应用状态。你将学习如何:

  • 在运行测试之前迁移和填充数据库种子
  • 通过事务或截断在单个测试之间清理数据库状态
  • 通过自动清理管理文件系统状态
  • 在测试之间重置 Redis 数据
  • 使用环境变量覆盖配置独立的测试数据库

概述

修改应用状态的测试(例如创建数据库记录、上传文件或在 Redis 中缓存数据)需要一种策略,以便在测试运行之间重置该状态。如果没有适当的清理,测试可能会相互干扰,导致根据执行顺序而时过时失败(flaky)的结果。

AdonisJS 通过 testUtils 服务提供处理常见状态管理模式的实用工具。一般的做法是:在全部测试之前运行一次数据库迁移,然后在单个测试之间使用事务或截断来重置数据。对于文件系统和 Redis,类似的模式可确保每个测试都从干净的状态开始。

Warning

请确保你的测试环境配置为使用与开发和生产环境不同的数据库和存储系统。针对生产数据运行测试可能导致数据丢失。你可以使用 .env.test 文件专门覆盖测试用的环境变量。

数据库状态管理

迁移数据库

tests/bootstrap.ts 中注册一个全局设置钩子,以便在任何测试执行之前运行迁移。testUtils.db().migrate() 方法会应用所有待处理的迁移,以准备数据库结构。

tests/bootstrap.ts
import testUtils from '@adonisjs/core/services/test_utils'

export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
  setup: [() => testUtils.db().migrate()],
  teardown: [],
}

如果你的应用使用多个数据库连接,请传入连接名称以定位特定的数据库。

tests/bootstrap.ts
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
  setup: [
    () => testUtils.db().migrate(),
    () => testUtils.db('tenant').migrate(),
  ],
  teardown: [],
}

填充数据库种子

如果你的测试需要种子数据,请在迁移之后添加 seed() 钩子。这会运行你的数据库 seeder 以用初始数据填充表。

tests/bootstrap.ts
import testUtils from '@adonisjs/core/services/test_utils'

export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
  setup: [
    () => testUtils.db().migrate(),
    () => testUtils.db().seed(),
  ],
  teardown: [],
}

在测试之间清理

虽然迁移全局只运行一次,但你需要在单个测试之间清理数据,以防止状态泄漏。AdonisJS 提供了两种方法:全局事务(global transactions)和截断(truncation)。

全局事务将测试内的所有数据库操作包裹在一个事务中,然后在测试完成时回滚。没有任何内容被实际持久化到数据库,这可以带来更快的测试执行速度。

tests/functional/users.spec.ts
import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'

test.group('Users', (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction())

  test('can create a user', async () => {
    // 此处的数据库变更会在测试结束后自动回滚
  })
})

withGlobalTransaction() 方法返回一个清理函数,Japa 会在每个测试之后自动调用它来回滚事务。

截断在测试之间清除表中的所有数据。这种方式实际删除了记录,而不是回滚事务。

tests/functional/posts.spec.ts
import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'

test.group('Posts', (group) => {
  group.each.setup(() => testUtils.db().truncate())

  test('can create a post', async () => {
    // 每个测试之前表都会被截断
  })
})

全局事务通常更快,特别是当你的数据库拥有很多表时,因为回滚一个事务比截断每个表开销更小。请选择最适合你测试需求的方法。

文件系统状态管理

对于创建文件的测试,请使用 @japa/file-system 插件。该插件提供用于管理文件的简单 API,并在每个测试之后自动清理它们。

将该插件作为开发依赖安装。

npm i -D @japa/file-system

在你的测试 bootstrap 文件中注册该插件。

tests/bootstrap.ts
import { fileSystem } from '@japa/file-system'

export const plugins: Config['plugins'] = [
  assert(),
  pluginAdonisJS(app),
  fileSystem(),
]

在测试中使用 fs 对象来创建文件。通过该 API 创建的任意文件都会在测试完成时自动删除。

tests/functional/uploads.spec.ts
import { test } from '@japa/runner'

test('can process an uploaded file', async ({ fs }) => {
  await fs.create('document.pdf', 'file contents')

  // 测试你的文件处理逻辑
  // 文件会在测试之后自动清理
})

文件被创建在该插件管理的临时目录中。更多配置选项和高级用法,请参阅 Japa file-system 插件文档

Redis 状态管理

对于与 Redis 交互的测试,请在测试之间清空测试数据库,以确保干净的状态。使用组设置钩子在每个测试之前调用 flushdb()

tests/functional/cache.spec.ts
import { test } from '@japa/runner'
import redis from '@adonisjs/redis/services/main'

test.group('Cache', (group) => {
  group.each.teardown(async () => {
    await redis.flushdb()
  })

  test('can cache a value', async () => {
    // 每个测试开始时 Redis 都是空的
  })
})

flushdb() 命令会清除当前所选 Redis 数据库中的所有键,而不影响同一 Redis 服务器上的其他数据库。请确保你的测试环境配置为使用与开发或生产环境不同的 Redis 数据库编号。

环境配置

使用 .env.test 文件专门覆盖测试环境的变量。该文件会在运行测试时自动加载。

.env.test
DB_DATABASE=my_app_test
REDIS_DB=1

这确保你的测试针对隔离的数据库运行,而不会危及你的开发或生产数据。