6月5日 00:26

How to test TypeORM? Including unit testing, integration testing, and mock strategies

Testing is an important part of ensuring code quality. TypeORM provides multiple testing strategies and tools, allowing developers to easily write and run tests.

Test Environment Configuration

Using In-Memory Database

typescript
import { DataSource } from 'typeorm'; import { User } from '../entity/User'; // Use SQLite in-memory database for testing export const testDataSource = new DataSource({ type: 'sqlite', database: ':memory:', // In-memory database entities: [User], synchronize: true, // Auto-sync table structure logging: false, dropSchema: true, // Clear database before each test }); // Test suite setup beforeAll(async () => { await testDataSource.initialize(); }); afterAll(async () => { await testDataSource.destroy(); }); // Clear database before each test beforeEach(async () => { await testDataSource.synchronize(true); });

Using Test Database

typescript
// Use separate test database export const testDataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'test_user', password: 'test_password', database: 'test_db', // Test database entities: [User, Post], synchronize: true, logging: false, }); // Configure test scripts in package.json { "scripts": { "test": "NODE_ENV=test jest", "test:watch": "NODE_ENV=test jest --watch", "test:coverage": "NODE_ENV=test jest --coverage" } }

Unit Testing

Testing Entities

typescript
import { 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); }); });

Testing Repositories

typescript
import { 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 () => { // Clear database 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(); }); });

Integration Testing

Testing Service Layer

typescript
import { 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 () => { // Clear database 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'); }); });

Testing Complex Queries

typescript
describe('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 and Stub

Mock Repository

typescript
import { Repository } from 'typeorm'; import { User } from '../entity/User'; describe('UserService with Mock Repository', () => { let userService: UserService; let userRepositoryMock: jest.Mocked<Repository<User>>; beforeEach(() => { // Create 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

typescript
import { 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(); }); });

Transaction Testing

Testing Transaction Rollback

typescript
describe('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); // Create initial user const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', }); await userRepository.save(user); const initialBalance = 1000; // Create account const account = accountRepository.create({ userId: user.id, balance: initialBalance, }); await accountRepository.save(account); // Test transaction rollback await expect( dataSource.transaction(async (manager) => { const userRepo = manager.getRepository(User); const accountRepo = manager.getRepository(Account); // Deduct const account = await accountRepo.findOne({ where: { userId: user.id }, }); account.balance -= 500; await accountRepo.save(account); // Simulate error throw new Error('Transaction failed'); }) ).rejects.toThrow('Transaction failed'); // Verify transaction rolled back 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); // Test transaction commit 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); }); // Verify transaction committed const finalAccount = await accountRepository.findOne({ where: { userId: user.id }, }); expect(finalAccount.balance).toBe(500); }); });

Testing Tools and Libraries

Using 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" } }

Using Supertest (API Testing)

typescript
import 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); // Random port }); 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'); }); });

Testing Best Practices

1. Isolate Tests

typescript
// ✅ Good: Each test is independent describe('User Repository', () => { beforeEach(async () => { // Clear database before each test await userRepository.clear(); }); it('test 1', async () => { // Independent test }); it('test 2', async () => { // Doesn't depend on test 1 }); }); // ❌ Bad: Tests depend on each other describe('User Repository', () => { let userId: number; it('test 1', async () => { const user = await userRepository.save({ name: 'John' }); userId = user.id; }); it('test 2', async () => { // Depends on test 1 result const user = await userRepository.findOne({ where: { id: userId } }); }); });

2. Use Test Data Factories

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)); } } // Use in tests 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. Test Edge Cases

typescript
describe('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, // Allow 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. Use Test Coverage

typescript
// Run tests and generate coverage report npm run test:coverage // Check coverage report // coverage/lcov-report/index.html // Set coverage threshold // jest.config.js coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }

TypeORM's testing strategies provide comprehensive testing support. Proper use of testing tools and best practices can ensure code quality and reliability.

标签:TypeORM