You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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();
  });
};

出现非确定性行为:多数时候提示某张表不存在,有时提示表已存在,偶尔能正常运行。

问题原因

  1. createTable的异步回调错误:Knex的schema.createTable回调不支持异步(async),添加async后,Knex会直接执行该异步函数但不会等待其完成,导致表结构配置(设置引擎、字段、外键)未完成就触发表创建,引发表结构异常或不存在的问题。
  2. 共享连接的并发冲突:若测试框架默认并行执行测试,多个用例共享同一个数据库连接和内存表,会导致删表、建表操作互相干扰,出现随机的“表已存在”或“表不存在”错误。
  3. 钩子函数资源清理不及时: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 21:17:14