使用Knex创建E2E测试数据库时出现非确定性问题的解决
问题与解决方案
问题描述
希望创建一个初始为空的数据库,用于每个E2E测试用例注入测试数据,使用Knex实现了以下代码:
const createTable = async ( name: string, callback: (table: Knex.CreateTableBuilder) => void ) => { return db.schema.createTable(name, async (table) => { table.engine("memory"); callback(table); }); }; const restartDb = async () => { await db.schema.dropTableIfExists("BudgetItem"); await db.schema.dropTableIfExists("Budget"); }; export const startDb = async () => { await restartDb(); await createTable("Budget", (table) => { // ...数据库 schema 定义 }); await createTable("BudgetItem", (table) => { // ...数据库 schema 定义 table .integer("budgetId") .unsigned() .notNullable() .references("id") .inTable("Budget"); // 外键关联 Budget 表 }); };
测试中调用以下钩子函数:
export const useTestDb = () => { beforeEach(async () => { await startDb(); }); afterAll(async () => { await db.destroy(); }); };
出现非确定性行为:多数时候提示某张表不存在,有时提示表已存在,偶尔能正常运行。
问题原因
- createTable的异步回调错误:Knex的
schema.createTable回调不支持异步(async),添加async后,Knex会直接执行该异步函数但不会等待其完成,导致表结构配置(设置引擎、字段、外键)未完成就触发表创建,引发表结构异常或不存在的问题。 - 共享连接的并发冲突:若测试框架默认并行执行测试,多个用例共享同一个数据库连接和内存表,会导致删表、建表操作互相干扰,出现随机的“表已存在”或“表不存在”错误。
- 钩子函数资源清理不及时:
afterAll仅在所有测试结束后销毁连接,并行测试时,前一个测试的连接未销毁就开始下一个测试,引发资源冲突。
正确实现方案
1. 修正createTable函数
去掉回调中的async关键字,Knex表结构配置为同步操作:
const createTable = async ( db: Knex, name: string, callback: (table: Knex.CreateTableBuilder) => void ) => { return db.schema.createTable(name, (table) => { table.engine("memory"); callback(table); }); };
2. 每个测试用独立数据库连接
避免共享全局连接,在beforeEach中创建新连接,afterEach中销毁:
import knex from 'knex'; export const useTestDb = () => { let db: Knex; beforeEach(async () => { // 为每个测试创建独立的数据库连接 db = knex({ // 根据你的数据库类型配置,示例为MySQL client: 'mysql', connection: { host: 'localhost', user: 'test_user', password: 'test_pass', database: 'test_db', }, }); await startDb(db); }); afterEach(async () => { // 每个测试结束后立即销毁连接 await db.destroy(); }); };
3. 调整startDb与restartDb,支持传入db实例
const restartDb = async (db: Knex) => { // 先删子表(带外键的表),再删父表,避免外键约束错误 await db.schema.dropTableIfExists("BudgetItem"); await db.schema.dropTableIfExists("Budget"); }; export const startDb = async (db: Knex) => { await restartDb(db); await createTable(db, "Budget", (table) => { table.increments('id').primary(); // 其他字段定义... }); await createTable(db, "BudgetItem", (table) => { table.increments('id').primary(); table .integer("budgetId") .unsigned() .notNullable() .references("id") .inTable("Budget"); // 其他字段定义... }); };
4. 可选:禁用测试并行
如果使用Jest,可在jest.config.js中设置串行执行,彻底避免并发冲突:
module.exports = { maxWorkers: 1, };
内容的提问来源于stack exchange,提问作者Vitor Figueredo Marques
相关产品推荐
相关产品推荐

