You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何避免Jest并行测试互相干扰数据库?

如何避免Jest并行测试互相干扰数据库?

这个问题我当初做Node.js/TypeScript集成测试的时候踩过好几次坑!并行测试确实能大幅提升速度,但数据库数据互相污染导致的诡异bug真的让人头大。分享几个我亲测有效的方案,从Jest配置到测试代码调整都有,你可以根据项目情况选:


1. 给每个测试进程分配独立的数据库实例/Schema(最彻底的方案)

Jest并行运行时会启动多个worker进程,我们可以给每个worker分配专属的数据库,从根源上避免数据冲突。核心是用Jest的globalSetupglobalTeardown和自定义testEnvironment来动态创建/销毁数据库:

步骤1:配置Jest

jest.config.js里指定全局初始化/清理脚本和自定义测试环境:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: './src/test/custom-test-env.ts',
  globalSetup: './src/test/global-setup.ts',
  globalTeardown: './src/test/global-teardown.ts',
  maxWorkers: '60%', // 根据你的机器性能调整并行数,比如CPU核心数的60%
};

步骤2:编写全局初始化脚本(global-setup.ts)

负责为当前worker进程创建专属数据库:

import { createDatabase } from '../utils/db-helpers';

export default async () => {
  // 用进程ID作为数据库名后缀,保证唯一性
  const testDbName = `myapp_test_${process.pid}`;
  // 调用你自己的数据库创建逻辑(比如用pg、Prisma或TypeORM的API)
  await createDatabase(testDbName);
  // 把数据库名存到全局变量,后续测试环境可以读取
  (global as any).TEST_DB_NAME = testDbName;
};

步骤3:编写全局清理脚本(global-teardown.ts)

测试全部结束后销毁专属数据库:

import { dropDatabase } from '../utils/db-helpers';

export default async () => {
  const testDbName = (global as any).TEST_DB_NAME;
  await dropDatabase(testDbName);
};

步骤4:自定义测试环境(custom-test-env.ts)

让当前worker的所有测试都连接到专属数据库:

import NodeEnvironment from 'jest-environment-node';
import { PrismaClient } from '@prisma/client';

class CustomTestEnv extends NodeEnvironment {
  async setup() {
    await super.setup();
    const testDbName = (global as any).TEST_DB_NAME;
    // 更新环境变量里的数据库连接字符串
    this.global.process.env.DATABASE_URL = `postgresql://username:password@localhost:5432/${testDbName}`;
    // 初始化Prisma客户端(如果用ORM的话)
    this.global.prisma = new PrismaClient({
      datasources: { db: { url: this.global.process.env.DATABASE_URL } }
    });
  }

  async teardown() {
    await this.global.prisma?.$disconnect();
    await super.teardown();
  }
}

export default CustomTestEnv;

2. 用事务回滚隔离每个测试用例

如果创建独立数据库成本太高(比如数据库权限限制),可以给每个测试用例套一层事务,测试结束后回滚,这样所有修改都不会持久化到数据库:

以Prisma为例的实现:

import { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;
let transaction: any;

beforeAll(async () => {
  prisma = new PrismaClient();
});

beforeEach(async () => {
  // 开启一个可回滚的事务
  transaction = await prisma.$transaction(
    async (tx) => tx,
    { isolationLevel: 'Serializable' }
  );
});

afterEach(async () => {
  // 回滚事务,丢弃所有测试修改
  await (prisma as any).$rollback(transaction);
});

afterAll(async () => {
  await prisma.$disconnect();
});

原生SQL的实现(比如PostgreSQL):

import { pool } from '../db-connection';

let client: any;

beforeEach(async () => {
  client = await pool.connect();
  // 创建保存点(替代嵌套事务,兼容更多数据库)
  await client.query('SAVEPOINT test_sp');
});

afterEach(async () => {
  // 回滚到保存点
  await client.query('ROLLBACK TO SAVEPOINT test_sp');
  client.release();
});

注意:部分数据库不支持嵌套事务,用保存点是更稳妥的方式。


3. 为测试数据添加唯一标识

如果前两种方案都不适合,退而求其次的方法是让每个测试用例的测试数据自带唯一标识,确保即使并行运行,数据也不会互相干扰:

// 生成唯一后缀的工具函数
const getUniqueSuffix = () => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

test('创建用户接口测试', async () => {
  const uniqueUsername = `test_user_${getUniqueSuffix()}`;
  // 用唯一用户名创建用户
  await request(app).post('/users').send({ username: uniqueUsername });
  
  // 查询时只针对当前测试的用户
  const user = await prisma.user.findUnique({ where: { username: uniqueUsername } });
  expect(user).not.toBeNull();
});

这种方式实现简单,但缺点是如果测试用例依赖全局数据(比如默认配置),还是可能出现冲突,适合小型测试套件。


4. 按需控制并行性(退而求其次的方案)

如果某些测试确实无法避免冲突,可以把这些测试标记为串行运行,其他测试保持并行:

给单个测试文件禁用并行:

在测试文件顶部添加:

// 强制这个文件的测试串行运行
jest.setTimeout(10000);
process.env.JEST_MAX_WORKERS = '1';

或者用Jest命令只串行跑特定测试:

npx jest --runInBand src/test/conflicting-tests/

最后总结

  • 优先选独立数据库/Schema:彻底解决冲突,适合中大型测试套件;
  • 次之选事务回滚:实现简单,性能开销小,适合对数据库操作不多的测试;
  • 临时方案选唯一标识数据:快速解决局部冲突,适合小型项目;
  • 实在不行再控制并行性:牺牲部分速度换稳定性。

我当初是从事务回滚过渡到独立数据库的,现在团队的集成测试并行跑完全没冲突,速度比串行快了3-4倍,亲测有效!

火山引擎 最新活动