3、数据库集成与故障模拟:配置TypeORM/Prisma测试数据库、使用事务回滚隔离测试数据、模拟数据库连接超时与断连

数据库是后端压测中最核心的依赖之一。在集成测试中,数据库配置不当会导致测试数据污染、测试间相互干扰,甚至压测结果失真。本章聚焦三个关键能力:测试数据库的独立配置、事务回滚隔离机制、以及数据库故障的模拟方法。掌握这些能力,才能让压测场景既真实又安全。

3.1 测试数据库的独立配置策略

在 NestJS 集成测试中,必须为测试环境配置独立的数据库实例或数据库名,避免与开发/生产数据库交叉。以下是 TypeORM 和 Prisma 的典型配置方式对比:

维度 TypeORM Prisma
配置方式 通过 DataSource 工厂函数动态创建 通过 .env.test 或 PrismaClient 实例化时传入
数据库命名 推荐使用 test_${数据库名} 前缀 推荐使用 test_${数据库名} 前缀
连接池大小 建议 poolSize: 5 避免压测时连接耗尽 通过 connectionLimit 参数控制
自动建表 synchronize: true(仅测试环境) prisma migrate deploy 或 db push
日志级别 建议关闭或设为 error 级别 建议关闭或设为 warn 级别
关键原则:测试数据库必须与开发数据库物理隔离。压测场景中,建议每次运行测试前重建数据库结构,确保基线一致。

TypeORM 测试数据库配置示例

// test/database/test-datasource.ts
import { DataSource } from 'typeorm';

export const createTestDataSource = async (): Promise<DataSource> => {
  const dataSource = new DataSource({
    type: 'postgres',
    host: process.env.TEST_DB_HOST || 'localhost',
    port: Number(process.env.TEST_DB_PORT) || 5432,
    username: process.env.TEST_DB_USER || 'test_user',
    password: process.env.TEST_DB_PASS || 'test_pass',
    database: process.env.TEST_DB_NAME || 'test_myapp',
    entities: ['src/**/*.entity.ts'],
    synchronize: true, // 测试环境自动同步表结构
    logging: false,    // 压测时关闭日志
    poolSize: 5,       // 限制连接池大小
  });
  await dataSource.initialize();
  return dataSource;
};

Prisma 测试数据库配置示例

// test/database/test-prisma.ts
import { PrismaClient } from '@prisma/client';

export const createTestPrisma = (): PrismaClient => {
  const prisma = new PrismaClient({
    datasources: {
      db: {
        url: process.env.TEST_DATABASE_URL || 
             'postgresql://test_user:test_pass@localhost:5432/test_myapp',
      },
    },
    log: ['error'], // 压测时只记录错误
  });
  return prisma;
};

3.2 事务回滚隔离测试数据

压测场景中,每个测试用例执行后必须清理数据,否则后续用例会受到污染。事务回滚是最高效的隔离方式:每个测试用例在一个数据库事务中执行,测试结束时回滚事务,数据自动恢复到测试前状态。

性能优势:事务回滚比逐条 DELETE 快 10~50 倍,尤其适合压测场景中大量数据写入的测试。

TypeORM 事务回滚实现

// test/helpers/typeorm-transaction.ts
import { DataSource, QueryRunner } from 'typeorm';

export class TransactionHelper {
  private queryRunner: QueryRunner;

  async start(dataSource: DataSource): Promise<void> {
    this.queryRunner = dataSource.createQueryRunner();
    await this.queryRunner.connect();
    await this.queryRunner.startTransaction();
  }

  async rollback(): Promise<void> {
    if (this.queryRunner) {
      await this.queryRunner.rollbackTransaction();
      await this.queryRunner.release();
    }
  }

  getManager() {
    return this.queryRunner.manager;
  }
}

Prisma 事务回滚实现

// test/helpers/prisma-transaction.ts
import { PrismaClient } from '@prisma/client';

export class PrismaTransactionHelper {
  private prisma: PrismaClient;

  constructor(prisma: PrismaClient) {
    this.prisma = prisma;
  }

  async executeInTransaction<T>(fn: (tx: any) => Promise<T>): Promise<T> {
    // Prisma 的交互式事务支持回滚
    return this.prisma.$transaction(async (tx) => {
      return fn(tx);
    }, {
      maxWait: 5000,    // 最大等待时间
      timeout: 10000,   // 事务超时时间
    });
  }
}
注意:Prisma 的交互式事务在回滚时不会自动抛出异常,需要在回调函数中手动 throw 来触发回滚。建议封装统一的回滚辅助函数。

3.3 模拟数据库连接超时与断连

压测场景必须覆盖数据库故障情况,验证应用在数据库不可用时的降级和恢复能力。以下是两种核心故障模拟方式:

数据库故障模拟架构 压测测试用例 连接超时模拟 connectionTimeout: 1000ms 断连模拟 kill connection / network down NestJS 数据库中间件 降级策略 缓存 / 熔断 / 重试 恢复验证 自动重连 / 健康检查 说明:故障模拟应在独立测试环境中进行,避免影响其他并行测试

3.3.1 模拟连接超时

通过配置数据库驱动层的超时参数,可以模拟连接超时场景。以下是 TypeORM 和 Prisma 的超时配置:

// TypeORM 连接超时配置
const dataSource = new DataSource({
  type: 'postgres',
  // ... 其他配置
  extra: {
    connectionTimeoutMillis: 1000, // 1秒超时
    query_timeout: 2000,           // 查询超时
    statement_timeout: 3000,       // 语句超时
  },
});

// Prisma 连接超时配置
const prisma = new PrismaClient({
  datasources: { db: { url: process.env.DATABASE_URL } },
  // Prisma 5.x 支持连接超时
  connectionTimeout: 1000, // 1秒超时
});
压测场景设计:在压测过程中动态修改数据库连接的超时参数,观察应用是否能够正确抛出 TimeoutError 并触发降级逻辑。

3.3.2 模拟数据库断连

断连模拟需要更底层的操作,常见方法包括:

  • 网络层断连:使用 iptables 或 tc 工具阻断数据库端口
  • 数据库层断连:执行 pg_terminate_backend 或 KILL CONNECTION
  • 应用层断连:在测试中手动关闭数据库连接池
// 使用 TypeORM 模拟断连
async function simulateDisconnection(dataSource: DataSource) {
  // 关闭所有连接
  await dataSource.destroy();
  
  // 验证应用是否进入降级模式
  // 然后重新初始化
  await dataSource.initialize();
}

// 使用 Prisma 模拟断连
async function simulatePrismaDisconnection(prisma: PrismaClient) {
  // 断开连接
  await prisma.$disconnect();
  
  // 验证降级逻辑
  // 重新连接
  await prisma.$connect();
}

3.4 故障恢复验证清单

在压测场景中,数据库故障恢复需要验证以下关键指标:

验证项 预期行为 验证方法
连接超时处理 返回 503 或自定义错误码 断言 HTTP 状态码和错误消息
断连后自动重连 在 5 秒内恢复连接 检查健康检查端点状态
事务回滚完整性 数据完全恢复到测试前 对比测试前后数据行数
降级策略生效 缓存数据返回而非数据库查询 检查缓存命中率指标
熔断器状态 故障时打开,恢复后关闭 监控熔断器状态变更日志
最佳实践:在压测场景中,建议将数据库故障模拟作为独立测试套件运行,与正常压测场景分开。故障模拟场景的并发量建议控制在 10~50 并发,避免真实数据库被压垮。

3.5 完整测试示例:事务回滚 + 故障模拟

以下是一个完整的 NestJS 集成测试示例,同时包含事务回滚和故障模拟:

// test/scenarios/database-fault.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { AppModule } from '../src/app.module';
import { TransactionHelper } from '../helpers/typeorm-transaction';
import { DataSource } from 'typeorm';

describe('Database Fault Recovery', () => {
  let app: INestApplication;
  let dataSource: DataSource;
  let transactionHelper: TransactionHelper;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();

    dataSource = app.get(DataSource);
    transactionHelper = new TransactionHelper();
  });

  beforeEach(async () => {
    await transactionHelper.start(dataSource);
  });

  afterEach(async () => {
    await transactionHelper.rollback();
  });

  it('should handle connection timeout gracefully', async () => {
    // 模拟连接超时
    const originalTimeout = dataSource.options.extra?.connectionTimeoutMillis;
    Object.assign(dataSource.options, {
      extra: { connectionTimeoutMillis: 1 }, // 1ms 超时
    });

    try {
      await dataSource.query('SELECT 1');
      fail('Should have thrown timeout error');
    } catch (error) {
      expect(error.message).toContain('timeout');
    }

    // 恢复配置
    Object.assign(dataSource.options, {
      extra: { connectionTimeoutMillis: originalTimeout },
    });
  });

  it('should recover after disconnection', async () => {
    // 模拟断连
    await dataSource.destroy();

    // 验证降级逻辑
    const healthCheck = await app.get('HealthCheckService').check();
    expect(healthCheck.database).toBe('unhealthy');

    // 恢复连接
    await dataSource.initialize();

    // 验证恢复
    const healthCheckAfter = await app.get('HealthCheckService').check();
    expect(healthCheckAfter.database).toBe('healthy');
  });
});
重要提醒:故障模拟测试必须在隔离的测试数据库上执行,切勿在生产数据库或共享开发数据库上运行。建议使用 Docker 容器化的临时数据库实例。

通过本章的配置和示例,你已经掌握了在 NestJS 集成测试中配置独立测试数据库、使用事务回滚隔离数据、以及模拟数据库连接超时与断连的核心技术。这些能力是构建可靠后端压测场景的基础,确保压测结果真实可信,同时保护测试环境的数据安全。