测试是保证代码质量的重要环节,TypeORM 提供了多种测试策略和工具,让开发者能够轻松编写和运行测试。
测试环境配置
使用内存数据库
typescriptimport { DataSource } from 'typeorm'; import { User } from '../entity/User'; // 使用 SQLite 内存数据库进行测试 export const testDataSource = new DataSource({ type: 'sqlite', database: ':memory:', // 内存数据库 entities: [User], synchronize: true, // 自动同步表结构 logging: false, dropSchema: true, // 每次测试前清空数据库 }); // 测试套件设置 beforeAll(async () => { await testDataSource.initialize(); }); afterAll(async () => { await testDataSource.destroy(); }); // 每个测试前清空数据库 beforeEach(async () => { await testDataSource.synchronize(true); });
使用测试数据库
typescript// 使用独立的测试数据库 export const testDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'test_user', password: 'test_password', database: 'test_db', // 测试数据库 entities: [User, Post], synchronize: true, logging: false, }); // 在 package.json 中配置测试脚本 { "scripts": { "test": "NODE_ENV=test jest", "test:watch": "NODE_ENV=test jest --watch", "test:coverage": "NODE_ENV=test jest --coverage" } }
单元测试
测试实体
typescriptimport { User } from '../entity/User'; describe('User Entity', () => { it('should create a user with valid data', () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; expect(user.name).toBe('John Doe'); expect(user.email).toBe('john@example.com'); expect(user.age).toBe(25); }); it('should validate email format', () => { const user = new User(); user.email = 'invalid-email'; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; expect(emailRegex.test(user.email)).toBe(false); }); it('should validate age range', () => { const user = new User(); user.age = 15; expect(user.age).toBeLessThan(18); }); });
测试 Repository
typescriptimport { DataSource } from 'typeorm'; import { User } from '../entity/User'; import { testDataSource } from './testDataSource'; describe('User Repository', () => { let userRepository: any; beforeAll(async () => { await testDataSource.initialize(); userRepository = testDataSource.getRepository(User); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { // 清空数据库 await userRepository.clear(); }); it('should create a user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); expect(savedUser.id).toBeDefined(); expect(savedUser.name).toBe('John Doe'); expect(savedUser.email).toBe('john@example.com'); }); it('should find user by id', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); const foundUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(foundUser).toBeDefined(); expect(foundUser.name).toBe('John Doe'); }); it('should find users by email', async () => { await userRepository.save({ name: 'John Doe', email: 'john@example.com', age: 25, }); const users = await userRepository.find({ where: { email: 'john@example.com' }, }); expect(users.length).toBe(1); expect(users[0].name).toBe('John Doe'); }); it('should update user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); savedUser.name = 'Jane Doe'; await userRepository.save(savedUser); const updatedUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(updatedUser.name).toBe('Jane Doe'); }); it('should delete user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); await userRepository.delete(savedUser.id); const deletedUser = await userRepository.findOne({ where: { id: savedUser.id }, }); expect(deletedUser).toBeNull(); }); });
集成测试
测试服务层
typescriptimport { UserService } from '../service/UserService'; import { testDataSource } from './testDataSource'; describe('UserService Integration Tests', () => { let userService: UserService; beforeAll(async () => { await testDataSource.initialize(); userService = new UserService(testDataSource); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { // 清空数据库 const userRepository = testDataSource.getRepository(User); await userRepository.clear(); }); it('should create user with validation', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; const user = await userService.createUser(userData); expect(user.id).toBeDefined(); expect(user.name).toBe('John Doe'); }); it('should throw error for invalid email', async () => { const userData = { name: 'John Doe', email: 'invalid-email', age: 25, }; await expect(userService.createUser(userData)).rejects.toThrow( 'Invalid email format' ); }); it('should find user by email', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; await userService.createUser(userData); const user = await userService.findByEmail('john@example.com'); expect(user).toBeDefined(); expect(user.name).toBe('John Doe'); }); });
测试复杂查询
typescriptdescribe('Complex Query Tests', () => { let userRepository: any; let postRepository: any; beforeAll(async () => { await testDataSource.initialize(); userRepository = testDataSource.getRepository(User); postRepository = testDataSource.getRepository(Post); }); afterAll(async () => { await testDataSource.destroy(); }); beforeEach(async () => { await userRepository.clear(); await postRepository.clear(); }); it('should find users with posts', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', }); const savedUser = await userRepository.save(user); await postRepository.save({ title: 'Post 1', author: savedUser, }); await postRepository.save({ title: 'Post 2', author: savedUser, }); const usersWithPosts = await userRepository.find({ relations: ['posts'], where: { id: savedUser.id }, }); expect(usersWithPosts[0].posts.length).toBe(2); }); it('should use query builder for complex queries', async () => { const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.age > :age', { age: 18 }) .orderBy('user.createdAt', 'DESC') .getMany(); expect(Array.isArray(users)).toBe(true); }); });
Mock 和 Stub
Mock Repository
typescriptimport { Repository } from 'typeorm'; import { User } from '../entity/User'; describe('UserService with Mock Repository', () => { let userService: UserService; let userRepositoryMock: jest.Mocked<Repository<User>>; beforeEach(() => { // 创建 Mock Repository userRepositoryMock = { create: jest.fn(), save: jest.fn(), findOne: jest.fn(), find: jest.fn(), delete: jest.fn(), } as any; userService = new UserService(userRepositoryMock); }); it('should create user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; userRepositoryMock.create.mockReturnValue(userData); userRepositoryMock.save.mockResolvedValue({ id: 1, ...userData, }); const user = await userService.createUser(userData); expect(userRepositoryMock.create).toHaveBeenCalledWith(userData); expect(userRepositoryMock.save).toHaveBeenCalled(); expect(user.id).toBe(1); }); it('should find user by email', async () => { const user = { id: 1, name: 'John Doe', email: 'john@example.com', age: 25, }; userRepositoryMock.findOne.mockResolvedValue(user); const foundUser = await userService.findByEmail('john@example.com'); expect(userRepositoryMock.findOne).toHaveBeenCalledWith({ where: { email: 'john@example.com' }, }); expect(foundUser).toEqual(user); }); });
Mock DataSource
typescriptimport { DataSource } from 'typeorm'; describe('Service with Mock DataSource', () => { let dataSourceMock: jest.Mocked<DataSource>; let service: MyService; beforeEach(() => { dataSourceMock = { getRepository: jest.fn(), createQueryBuilder: jest.fn(), transaction: jest.fn(), } as any; service = new MyService(dataSourceMock); }); it('should use repository from data source', async () => { const repositoryMock = { find: jest.fn().mockResolvedValue([]), }; dataSourceMock.getRepository.mockReturnValue(repositoryMock); await service.findAll(); expect(dataSourceMock.getRepository).toHaveBeenCalledWith(User); expect(repositoryMock.find).toHaveBeenCalled(); }); });
事务测试
测试事务回滚
typescriptdescribe('Transaction Tests', () => { let dataSource: DataSource; beforeAll(async () => { dataSource = new DataSource({ type: 'sqlite', database: ':memory:', entities: [User, Account], synchronize: true, }); await dataSource.initialize(); }); afterAll(async () => { await dataSource.destroy(); }); it('should rollback transaction on error', async () => { const userRepository = dataSource.getRepository(User); const accountRepository = dataSource.getRepository(Account); // 创建初始用户 const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', }); await userRepository.save(user); const initialBalance = 1000; // 创建账户 const account = accountRepository.create({ userId: user.id, balance: initialBalance, }); await accountRepository.save(account); // 测试事务回滚 await expect( dataSource.transaction(async (manager) => { const userRepo = manager.getRepository(User); const accountRepo = manager.getRepository(Account); // 扣款 const account = await accountRepo.findOne({ where: { userId: user.id }, }); account.balance -= 500; await accountRepo.save(account); // 模拟错误 throw new Error('Transaction failed'); }) ).rejects.toThrow('Transaction failed'); // 验证事务已回滚 const finalAccount = await accountRepository.findOne({ where: { userId: user.id }, }); expect(finalAccount.balance).toBe(initialBalance); }); it('should commit transaction on success', async () => { const userRepository = dataSource.getRepository(User); const accountRepository = dataSource.getRepository(Account); const user = userRepository.create({ name: 'Jane Doe', email: 'jane@example.com', }); await userRepository.save(user); const initialBalance = 1000; const account = accountRepository.create({ userId: user.id, balance: initialBalance, }); await accountRepository.save(account); // 测试事务提交 await dataSource.transaction(async (manager) => { const accountRepo = manager.getRepository(Account); const account = await accountRepo.findOne({ where: { userId: user.id }, }); account.balance -= 500; await accountRepo.save(account); }); // 验证事务已提交 const finalAccount = await accountRepository.findOne({ where: { userId: user.id }, }); expect(finalAccount.balance).toBe(500); }); });
测试工具和库
使用 Jest
typescript// jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], transform: { '^.+\\.ts$': 'ts-jest', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }, }; // package.json { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" } }
使用 Supertest (API 测试)
typescriptimport request from 'supertest'; import { app } from '../app'; import { testDataSource } from './testDataSource'; describe('User API Tests', () => { let server: any; beforeAll(async () => { await testDataSource.initialize(); server = app.listen(0); // 随机端口 }); afterAll(async () => { await testDataSource.destroy(); server.close(); }); beforeEach(async () => { const userRepository = testDataSource.getRepository(User); await userRepository.clear(); }); it('should create user via POST', async () => { const response = await request(server) .post('/api/users') .send({ name: 'John Doe', email: 'john@example.com', age: 25, }) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe('John Doe'); }); it('should get user by ID', async () => { const userRepository = testDataSource.getRepository(User); const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: 25, }); const savedUser = await userRepository.save(user); const response = await request(server) .get(`/api/users/${savedUser.id}`) .expect(200); expect(response.body.id).toBe(savedUser.id); expect(response.body.name).toBe('John Doe'); }); it('should return 404 for non-existent user', async () => { const response = await request(server) .get('/api/users/999') .expect(404); expect(response.body).toHaveProperty('error'); }); });
测试最佳实践
1. 隔离测试
typescript// ✅ 好的做法:每个测试独立 describe('User Repository', () => { beforeEach(async () => { // 每个测试前清空数据库 await userRepository.clear(); }); it('test 1', async () => { // 独立的测试 }); it('test 2', async () => { // 不依赖 test 1 }); }); // ❌ 不好的做法:测试相互依赖 describe('User Repository', () => { let userId: number; it('test 1', async () => { const user = await userRepository.save({ name: 'John' }); userId = user.id; }); it('test 2', async () => { // 依赖 test 1 的结果 const user = await userRepository.findOne({ where: { id: userId } }); }); });
2. 使用测试数据工厂
typescript// factory/UserFactory.ts export class UserFactory { static create(overrides: Partial<User> = {}): User { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; Object.assign(user, overrides); return user; } static createMany(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, () => this.create(overrides)); } } // 在测试中使用 describe('User Repository', () => { it('should create user', async () => { const user = UserFactory.create({ name: 'Jane Doe', email: 'jane@example.com', }); const savedUser = await userRepository.save(user); expect(savedUser.name).toBe('Jane Doe'); }); it('should create multiple users', async () => { const users = UserFactory.createMany(5); const savedUsers = await userRepository.save(users); expect(savedUsers.length).toBe(5); }); });
3. 测试边界情况
typescriptdescribe('User Repository Edge Cases', () => { it('should handle empty results', async () => { const users = await userRepository.find({ where: { name: 'NonExistent' }, }); expect(users).toEqual([]); }); it('should handle null values', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', age: null, // 允许 null }); const savedUser = await userRepository.save(user); expect(savedUser.age).toBeNull(); }); it('should handle duplicate emails', async () => { const userData = { name: 'John Doe', email: 'john@example.com', age: 25, }; await userRepository.save(userRepository.create(userData)); await expect( userRepository.save(userRepository.create(userData)) ).rejects.toThrow(); }); });
4. 使用测试覆盖率
typescript// 运行测试并生成覆盖率报告 npm run test:coverage // 检查覆盖率报告 // coverage/lcov-report/index.html // 设置覆盖率阈值 // jest.config.js coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }
TypeORM 的测试策略提供了全面的测试支持,合理使用测试工具和最佳实践可以确保代码质量和可靠性。