TypeORM测试:Mock Repository、SQLite内存数据库和NestJS集成
TypeORM 的测试分两层:不依赖数据库的纯逻辑测试(单元测试),和需要真实数据库交互的测试(集成测试)。很多人所有测试都连数据库,跑得又慢又不稳定;也有人不连数据库,Mock 了一大堆,测完发现线上还是出 bug。这篇文章把两种策略的使用场景和实现方式都讲清楚。
测试环境:SQLite 内存数据库
集成测试需要真实数据库,但不需要用 MySQL/PostgreSQL——SQLite 内存数据库够用,速度快 10 倍以上,每个测试文件启动不到 100ms:
typescriptimport { DataSource } from 'typeorm'; import { User } from '../entity/User'; import { Post } from '../entity/Post'; export const testDataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User, Post], synchronize: true, // 自动建表 logging: false, });
为什么用 SQLite 而不是真实数据库:
- 零配置:不需要装数据库、建测试库、管连接字符串
- 隔离性:内存数据库每次测试完自动销毁,测试之间无干扰
- 速度:内存操作,无网络 IO,单测 < 50ms
但 SQLite 不支持某些 MySQL/PostgreSQL 特性(如 RETURNING、ON CONFLICT、JSON 函数)。如果你的查询用了这些特性,需要用真实的 PostgreSQL 做集成测试。
测试基础设施
typescript// test/setup.ts import { DataSource } from 'typeorm'; let dataSource: DataSource; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [__dirname + '/../src/entity/*.ts'], synchronize: true, }); await dataSource.initialize(); }); afterAll(async () => { await dataSource.destroy(); }); // 每个测试前清空所有表 afterEach(async () => { const entities = dataSource.entityMetadatas; for (const entity of entities) { const repository = dataSource.getRepository(entity.name); await repository.clear(); } });
单元测试:不连数据库
单元测试只测业务逻辑,不测数据库交互。Repository 方法用 Mock 替代。
Mock Repository
typescriptimport { UsersService } from './users.service'; import { Repository } from 'typeorm'; import { User } from './user.entity'; describe('UsersService', () => { let service: UsersService; let mockRepository: jest.Mocked<Repository<User>>; beforeEach(() => { // 创建 Mock Repository mockRepository = { find: jest.fn(), findOne: jest.fn(), create: jest.fn(), save: jest.fn(), remove: jest.fn(), count: jest.fn(), } as any; service = new UsersService(mockRepository); }); it('should return all users', async () => { const mockUsers = [ { id: 1, name: 'Alice', email: 'alice@test.com' }, { id: 2, name: 'Bob', email: 'bob@test.com' }, ]; mockRepository.find.mockResolvedValue(mockUsers as User[]); const result = await service.findAll(); expect(result).toEqual(mockUsers); expect(mockRepository.find).toHaveBeenCalled(); }); it('should create a user', async () => { const dto = { name: 'Alice', email: 'alice@test.com' }; const savedUser = { id: 1, ...dto }; mockRepository.create.mockReturnValue(savedUser as User); mockRepository.save.mockResolvedValue(savedUser as User); const result = await service.create(dto); expect(result).toEqual(savedUser); expect(mockRepository.create).toHaveBeenCalledWith(dto); expect(mockRepository.save).toHaveBeenCalled(); }); it('should throw when user not found', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.findOne(999)).rejects.toThrow('User not found'); }); });
Mock 的核心原则:只 Mock 外部依赖(数据库),不 Mock 被测代码本身的逻辑。如果 Service 里有个计算函数,直接测它,不要 Mock。
什么时候用 Mock,什么时候不用
| 场景 | 策略 | 原因 |
|---|---|---|
| 纯逻辑函数 | 不 Mock | 没有外部依赖 |
| Service → Repository | Mock Repository | 隔离数据库,测试快 |
| 复杂查询逻辑 | 不 Mock,用集成测试 | Mock 查询结果无法验证 SQL 正确性 |
| 第三方 API | Mock HTTP | 不能调外部服务 |
关键判断:你的测试目的是验证"代码逻辑对不对"还是"SQL 查询对不对"?前者用 Mock,后者用集成测试。
集成测试:连真实数据库
集成测试验证 SQL 是否正确、事务是否生效、约束是否有效——这些 Mock 测不了。
事务回滚策略
每个测试跑在一个事务里,测完回滚,数据库回到干净状态:
typescriptdescribe('User integration tests', () => { let dataSource: DataSource; let userRepository: Repository<User>; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }); await dataSource.initialize(); userRepository = dataSource.getRepository(User); }); afterAll(async () => { await dataSource.destroy(); }); it('should create and find a user', async () => { const user = userRepository.create({ name: 'Alice', email: 'alice@test.com', }); await userRepository.save(user); const found = await userRepository.findOne({ where: { email: 'alice@test.com' }, }); expect(found).toBeDefined(); expect(found!.name).toBe('Alice'); }); it('should enforce unique email constraint', async () => { await userRepository.save({ name: 'Alice', email: 'alice@test.com' }); await expect( userRepository.save({ name: 'Bob', email: 'alice@test.com' }) ).rejects.toThrow(); // SQLite 会抛 UNIQUE constraint 错误 }); });
测试复杂查询
Query Builder 的复杂查询必须用集成测试——Mock 的 find 返回值证明不了 SQL 写对了:
typescriptit('should find users with post count > 5', async () => { // 准备数据 const user = await userRepository.save({ name: 'Alice', email: 'a@test.com' }); for (let i = 0; i < 6; i++) { await postRepository.save({ title: `Post ${i}`, authorId: user.id }); } // 执行复杂查询 const result = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .groupBy('user.id') .having('COUNT(post.id) > 5') .getMany(); expect(result).toHaveLength(1); expect(result[0].name).toBe('Alice'); });
NestJS + TypeORM 测试
NestJS 项目里,TypeORM 通过模块注入,测试需要用 @nestjs/testing:
单元测试(Mock DataSource)
typescriptimport { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { UsersService } from './users.service'; import { User } from './user.entity'; describe('UsersService', () => { let service: UsersService; let mockRepo: any; beforeEach(async () => { mockRepo = { find: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue(null), save: jest.fn().mockResolvedValue({}), create: jest.fn().mockReturnValue({}), }; const module = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }, ], }).compile(); service = module.get<UsersService>(UsersService); }); it('should be defined', () => { expect(service).toBeDefined(); }); });
getRepositoryToken(User) 是关键——NestJS 用这个 token 注入 Repository,Mock 时也用同一个 token。
E2E 测试(真实数据库)
typescriptimport { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import * as request from 'supertest'; describe('Users API (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }), TypeOrmModule.forFeature([User]), UsersModule, ], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); afterAll(async () => { await app.close(); }); it('POST /users', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'Alice', email: 'alice@test.com' }) .expect(201); }); it('GET /users', () => { return request(app.getHttpServer()) .get('/users') .expect(200); }); });
E2E 测试用 SQLite 内存数据库,请求走完整的 HTTP → Controller → Service → Repository → 数据库链路,最接近真实场景。
测试策略总结
| 测试类型 | 用途 | 速度 | 数据库 | 覆盖范围 |
|---|---|---|---|---|
| 单元测试 | 验证业务逻辑 | < 10ms | 不需要(Mock) | Service 层逻辑 |
| 集成测试 | 验证 SQL 和约束 | 50-200ms | SQLite 内存 | Repository 查询 |
| E2E 测试 | 验证完整请求链路 | 100-500ms | SQLite 内存 | HTTP → 数据库 |
经验法则:单元测试占 70%,集成测试占 20%,E2E 测试占 10%。核心业务逻辑用单元测试覆盖,复杂查询用集成测试验证,API 入口用少量 E2E 测试保证链路畅通。