【Nest】集成测试

什么是集成测试(Integration Test)

  • 定义 :集成测试在单元测试之上,验证模块之间真实交互是否正确(例如 service 与 repository、controller 与 service 的真实组合),不把内部重要依赖 mock 掉(或只 mock 外部第三方服务),但通常仍避免对生产外部系统(真实第三方 API、生产 DB)进行依赖。
  • 目的:检验不同层/模块的组合行为(比如 ORM 查询能否按预期返回、controller 路由到 service 并把 DB 数据正确返回等),比单元测试更能发现集成面的错误,但比 e2e 更局部、更快。

常见场景

  1. Service + Repository 的集成测试:真实使用测试数据库(如 sqlite in-memory / test container DB),验证 ORM 查询、实体映射、事务行为。
  2. Controller HTTP 层集成测试(通常也称为部分 e2e) :启动 INestApplication(或 FastifyAdapter 等),通过 supertest 发 HTTP 请求,验证路由、管道、守卫和 service 的集成,但仍可用测试 DB、或 mock 外部服务。

集成测试的两种常见实现方式(对比)

  • 使用真实测试 DB(推荐):例如 sqlite 内存或单独的测试 Postgres(Docker / Testcontainers)。优点:接近真实;缺点:需要注意隔离、启动速度。
  • 用部分 mock(例如 mock 外部 API、但真实 DB):保持 DB 真实、网络/邮件/第三方被 mock,常见于业务有外部依赖时。

示例项目说明

假设有 User 实体、UsersServiceUsersController,使用 TypeORM。我们做两个集成测试:

  1. users.service.int-spec.ts ------ Service + Repository(TypeORM sqlite 内存)
  2. users.e2e-spec.ts ------ 启动 Nest 应用并用 supertest 调用 HTTP 接口(使用同样的测试 DB)

代码示例

先给出最小实现用于测试:

ts 复制代码
// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}
ts 复制代码
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { InjectRepository } from '@nestjs/typeorm';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly repo: Repository<User>,
  ) {}

  create(name: string) {
    const u = this.repo.create({ name });
    return this.repo.save(u);
  }

  findOne(id: number) {
    return this.repo.findOneBy({ id });
  }

  findAll() {
    return this.repo.find();
  }
}
ts 复制代码
// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly svc: UsersService) {}

  @Post()
  create(@Body('name') name: string) {
    return this.svc.create(name);
  }

  @Get()
  list() {
    return this.svc.findAll();
  }

  @Get(':id')
  get(@Param('id') id: string) {
    return this.svc.findOne(Number(id));
  }
}
ts 复制代码
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

集成测试 A:Service + Repository(TypeORM + sqlite :memory:)

重点:在测试 TestingModule导入 TypeOrmModule.forRoot 指向 sqlite 内存数据库 ,并导入 UsersModule。设 synchronize: truedropSchema: true(测试专用)以保证干净的 schema。

ts 复制代码
// test/users.service.int-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from '../src/users/users.service';
import { User } from '../src/users/user.entity';

describe('UsersService (integration)', () => {
  let moduleRef: TestingModule;
  let service: UsersService;
  let repo: Repository<User>;

  beforeAll(async () => {
    moduleRef = await Test.createTestingModule({
      imports: [
        // 连接到 sqlite in-memory 数据库,仅供测试使用
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          dropSchema: true,      // 测试时重建 schema
          entities: [User],
          synchronize: true,     // 测试时自动同步实体(仅测试)
          logging: false,
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UsersService],
    }).compile();

    service = moduleRef.get<UsersService>(UsersService);
    repo = moduleRef.get<Repository<User>>(getRepositoryToken(User));
  });

  afterAll(async () => {
    // 关闭连接
    await moduleRef.close();
  });

  beforeEach(async () => {
    // 测试隔离:清空表(或使用事务回滚策略,见后文)
    await repo.clear();
  });

  it('should create and find user', async () => {
    const created = await service.create('alice');
    expect(created).toHaveProperty('id');
    expect(created.name).toBe('alice');

    const found = await service.findOne(created.id);
    expect(found.name).toBe('alice');
  });

  it('findAll returns multiple users', async () => {
    await service.create('u1');
    await service.create('u2');
    const all = await service.findAll();
    expect(all.length).toBe(2);
    const names = all.map(u => u.name).sort();
    expect(names).toEqual(['u1', 'u2']);
  });
});

要点解释

  • 使用 sqlite :memory: 不会写磁盘,速度快。
  • dropSchema: true + synchronize: true 可在测试每次启动时保证 schema 干净(注意:不要在生产使用)。
  • repo.clear() 在每个 beforeEach 中清数据保证测试隔离(也可以用事务回滚替代)。

集成测试 B:Controller HTTP 层集成测试(用 supertest)

这类测试会启动 INestApplication,并使用 supertest 发起 HTTP 请求。仍然用 sqlite 内存 DB。也可以在测试时 overrideProvider 来 mock发送邮件、第三方 API 等。

ts 复制代码
// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from '../src/users/users.module';
import { User } from '../src/users/user.entity';

describe('UsersController (e2e/integration)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          dropSchema: true,
          entities: [User],
          synchronize: true,
        }),
        UsersModule,
      ],
    }).compile();

    app = moduleFixture.createNestApplication();
    // 如果你在 controller 使用 ValidationPipe,记得在测试中也加上
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/users (POST) -> create and GET /users', async () => {
    const server = app.getHttpServer();
    // 创建用户
    await request(server)
      .post('/users')
      .send({ name: 'bob' })
      .expect(201)
      .then(res => {
        expect(res.body).toHaveProperty('id');
        expect(res.body.name).toBe('bob');
      });

    // 列表
    const list = await request(server).get('/users').expect(200);
    expect(Array.isArray(list.body)).toBe(true);
    expect(list.body.length).toBe(1);
    expect(list.body[0].name).toBe('bob');
  });

  it('/users/:id (GET) -> 404 for missing', async () => {
    const server = app.getHttpServer();
    await request(server).get('/users/9999').expect(200); // 注意:Our service returns null; 若要返回 404 可在 controller 里处理
  });
});

注意 :上面示例中 GET /users/9999 返回的状态取决于 controller/service 是否将 null 转化为 404;真实项目中你可能在 service 抛出 NotFoundException 或在 controller 中判断并抛 404。调整断言以匹配你的实现。

相关推荐
蓝色空白的博客8 天前
自动化测试脚本-->集成测试部署思路整理(1)
python·集成测试
Kiri霧9 天前
在actix-web应用用构建集成测试
后端·rust·集成测试
Turboex邮件分享22 天前
Syslog日志集成搭建
运维·elasticsearch·集成测试
seabirdssss24 天前
针对单元测试、集成测试、系统测试和验收测试(用户测试)各自的目标和测试内容不同,设计对应的各类测试用例
单元测试·测试用例·集成测试
tianyuanwo24 天前
构建质量的堡垒:一文读懂单元测试、集成测试、系统测试与回归测试
单元测试·集成测试·系统测试·回归测试
Sic_MOS_780168241 个月前
超高密度2kW GaN基低压电机驱动器的设计
人工智能·经验分享·汽车·集成测试·硬件工程·能源
cadereliu1 个月前
软件测试 - 接口测试(上篇)
集成测试
SXTomi1 个月前
【无人机】无人机用户体验测试策略详细介绍
集成测试·无人机·用户体验