Integration_testing
Chapter 42: Integration Testing
Section titled “Chapter 42: Integration Testing”Testing TypeORM with Real Database Connections
Section titled “Testing TypeORM with Real Database Connections”42.1 Integration Testing Overview
Section titled “42.1 Integration Testing Overview”Integration tests verify that multiple components work together correctly, including database interactions.
Integration Testing Scope ================================================================================
Unit Tests (Mocked): +------------------+ | Service | <-- Mocked repository +------------------+
Integration Tests (Real): +------------------+ +------------------+ +------------------+ | Controller | --> | Service | --> | Database | +------------------+ +------------------+ +------------------+
What Integration Tests Verify: - Entity mappings - Repository queries - Transaction behavior - Relationship loading - Query performance
================================================================================42.2 Test Database Setup
Section titled “42.2 Test Database Setup”Test Database Configuration
Section titled “Test Database Configuration”import { DataSource } from 'typeorm';import { config } from 'dotenv';
config({ path: '.env.test' });
export const testDataSource = new DataSource({ type: 'postgres', host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT, 10) || 5432, username: process.env.DB_USERNAME || 'postgres', password: process.env.DB_PASSWORD || 'postgres', database: process.env.DB_DATABASE || 'myapp_test', entities: ['src/**/*.entity{.ts,.js}'], migrations: ['src/migrations/*{.ts,.js}'], synchronize: false, // Use migrations dropSchema: false, // Don't drop schema logging: false,});
// test/test-database.service.tsimport { DataSource } from 'typeorm';
export class TestDatabaseService { private static dataSource: DataSource;
static async getDataSource(): Promise<DataSource> { if (!this.dataSource) { this.dataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', database: 'myapp_test', entities: ['src/**/*.entity{.ts,.js}'], synchronize: true, // Create schema for tests dropSchema: true, // Drop and recreate logging: false, });
await this.dataSource.initialize(); }
return this.dataSource; }
static async cleanup(): Promise<void> { if (this.dataSource?.isInitialized) { await this.dataSource.destroy(); this.dataSource = null as any; } }
static async clearTables(): Promise<void> { const dataSource = await this.getDataSource(); const entities = dataSource.entityMetadatas;
for (const entity of entities) { const repository = dataSource.getRepository(entity.name); await repository.query(`TRUNCATE "${entity.tableName}" CASCADE`); } }}Jest Setup for Integration Tests
Section titled “Jest Setup for Integration Tests”import { TestDatabaseService } from './test-database.service';
global.beforeAll(async () => { await TestDatabaseService.getDataSource();});
global.afterAll(async () => { await TestDatabaseService.cleanup();});
global.beforeEach(async () => { await TestDatabaseService.clearTables();});
// jest-integration.config.jsmodule.exports = { moduleFileExtensions: ['js', 'json', 'ts'], rootDir: '.', testRegex: '.integration.spec.ts$', transform: { '^.+\\.(t|j)s$': 'ts-jest', }, testEnvironment: 'node', setupFilesAfterEnv: ['./jest-integration.setup.ts'], testTimeout: 30000, // Longer timeout for integration tests};42.3 Testing Repositories
Section titled “42.3 Testing Repositories”Repository Integration Test
Section titled “Repository Integration Test”import { Test, TestingModule } from '@nestjs/testing';import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';import { Post } from '../posts/post.entity';import { TestDatabaseService } from '../../test/test-database.service';
describe('UserRepository (Integration)', () => { let userRepository: Repository<User>; let postRepository: Repository<Post>;
beforeAll(async () => { const dataSource = await TestDatabaseService.getDataSource();
const module: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () => ({ ...dataSource.options, }), }), TypeOrmModule.forFeature([User, Post]), ], }).compile();
userRepository = module.get(getRepositoryToken(User)); postRepository = module.get(getRepositoryToken(Post)); });
afterAll(async () => { await TestDatabaseService.cleanup(); });
beforeEach(async () => { await TestDatabaseService.clearTables(); });
describe('CRUD operations', () => { it('should create a user', async () => { const user = userRepository.create({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
const saved = await userRepository.save(user);
expect(saved.id).toBeDefined(); expect(saved.name).toBe('John Doe'); expect(saved.email).toBe('john@example.com'); expect(saved.createdAt).toBeDefined(); });
it('should find user by email', async () => { await userRepository.save({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
const found = await userRepository.findOne({ where: { email: 'john@example.com' }, });
expect(found).toBeDefined(); expect(found?.name).toBe('John Doe'); });
it('should update user', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
user.name = 'Jane Doe'; const updated = await userRepository.save(user);
expect(updated.name).toBe('Jane Doe'); });
it('should delete user', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
await userRepository.delete(user.id);
const found = await userRepository.findOne({ where: { id: user.id }, });
expect(found).toBeNull(); }); });
describe('Relationships', () => { it('should load user with posts', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
await postRepository.save([ { title: 'Post 1', content: 'Content 1', authorId: user.id, }, { title: 'Post 2', content: 'Content 2', authorId: user.id, }, ]);
const userWithPosts = await userRepository.findOne({ where: { id: user.id }, relations: ['posts'], });
expect(userWithPosts?.posts).toHaveLength(2); expect(userWithPosts?.posts[0].title).toBe('Post 1'); }); });
describe('QueryBuilder', () => { it('should find users with post count', async () => { const user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', password: 'hashed_password', });
await postRepository.save([ { title: 'Post 1', content: 'Content 1', authorId: user.id, }, { title: 'Post 2', content: 'Content 2', authorId: user.id, }, ]);
const result = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .select(['user.id', 'user.name']) .addSelect('COUNT(post.id)', 'postCount') .groupBy('user.id') .getRawOne();
expect(result.postCount).toBe('2'); }); });});42.4 Testing Services with Database
Section titled “42.4 Testing Services with Database”Service Integration Test
Section titled “Service Integration Test”import { Test, TestingModule } from '@nestjs/testing';import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';import { UsersService } from './users.service';import { User } from './user.entity';import { Post } from '../posts/post.entity';import { TestDatabaseService } from '../../test/test-database.service';import { NotFoundException, ConflictException } from '@nestjs/common';
describe('UsersService (Integration)', () => { let service: UsersService; let module: TestingModule;
beforeAll(async () => { const dataSource = await TestDatabaseService.getDataSource();
module = await Test.createTestingModule({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () => ({ ...dataSource.options }), }), TypeOrmModule.forFeature([User, Post]), ], providers: [UsersService], }).compile();
service = module.get(UsersService); });
afterAll(async () => { await module.close(); await TestDatabaseService.cleanup(); });
beforeEach(async () => { await TestDatabaseService.clearTables(); });
describe('create', () => { it('should create user with hashed password', async () => { const dto = { name: 'John Doe', email: 'john@example.com', password: 'password123', };
const user = await service.create(dto);
expect(user.id).toBeDefined(); expect(user.name).toBe('John Doe'); expect(user.password).not.toBe('password123'); // Should be hashed });
it('should throw on duplicate email', async () => { await service.create({ name: 'John Doe', email: 'john@example.com', password: 'password123', });
await expect( service.create({ name: 'Jane Doe', email: 'john@example.com', password: 'password456', }), ).rejects.toThrow(ConflictException); }); });
describe('findOne', () => { it('should return user with relations', async () => { const user = await service.create({ name: 'John Doe', email: 'john@example.com', password: 'password123', });
const found = await service.findOne(user.id);
expect(found).toBeDefined(); expect(found.name).toBe('John Doe'); });
it('should throw NotFoundException', async () => { await expect(service.findOne(999)).rejects.toThrow(NotFoundException); }); });
describe('update', () => { it('should update user', async () => { const user = await service.create({ name: 'John Doe', email: 'john@example.com', password: 'password123', });
const updated = await service.update(user.id, { name: 'Jane Doe' });
expect(updated.name).toBe('Jane Doe'); }); });
describe('remove', () => { it('should delete user', async () => { const user = await service.create({ name: 'John Doe', email: 'john@example.com', password: 'password123', });
await service.remove(user.id);
await expect(service.findOne(user.id)).rejects.toThrow(NotFoundException); }); });});42.5 Testing Transactions
Section titled “42.5 Testing Transactions”Transaction Integration Test
Section titled “Transaction Integration Test”import { Test, TestingModule } from '@nestjs/testing';import { TypeOrmModule, InjectDataSource } from '@nestjs/typeorm';import { DataSource } from 'typeorm';import { OrdersService } from './orders.service';import { Order } from './order.entity';import { Product } from '../products/product.entity';import { TestDatabaseService } from '../../test/test-database.service';
describe('OrdersService Transactions (Integration)', () => { let service: OrdersService; let dataSource: DataSource; let module: TestingModule;
beforeAll(async () => { const testDataSource = await TestDatabaseService.getDataSource();
module = await Test.createTestingModule({ imports: [ TypeOrmModule.forRootAsync({ useFactory: () => ({ ...testDataSource.options }), }), TypeOrmModule.forFeature([Order, Product]), ], providers: [OrdersService], }).compile();
service = module.get(OrdersService); dataSource = module.get(DataSource); });
afterAll(async () => { await module.close(); await TestDatabaseService.cleanup(); });
beforeEach(async () => { await TestDatabaseService.clearTables(); });
describe('createOrder', () => { it('should create order and update inventory atomically', async () => { // Setup product const productRepo = dataSource.getRepository(Product); const product = await productRepo.save({ name: 'Product 1', price: 100, stock: 10, });
// Create order const order = await service.createOrder({ items: [{ productId: product.id, quantity: 2 }], });
// Verify order created expect(order.id).toBeDefined(); expect(order.total).toBe(200);
// Verify inventory updated const updatedProduct = await productRepo.findOne({ where: { id: product.id }, }); expect(updatedProduct?.stock).toBe(8); });
it('should rollback on insufficient stock', async () => { // Setup product with low stock const productRepo = dataSource.getRepository(Product); const product = await productRepo.save({ name: 'Product 1', price: 100, stock: 1, });
// Try to create order for more than available await expect( service.createOrder({ items: [{ productId: product.id, quantity: 5 }], }), ).rejects.toThrow('Insufficient stock');
// Verify no order created const orderRepo = dataSource.getRepository(Order); const orders = await orderRepo.find(); expect(orders).toHaveLength(0);
// Verify stock unchanged const unchangedProduct = await productRepo.findOne({ where: { id: product.id }, }); expect(unchangedProduct?.stock).toBe(1); }); });
describe('transferFunds', () => { it('should transfer funds atomically', async () => { // Setup accounts const accountRepo = dataSource.getRepository(Account); const from = await accountRepo.save({ balance: 1000 }); const to = await accountRepo.save({ balance: 500 });
// Transfer await service.transferFunds(from.id, to.id, 200);
// Verify balances const fromAfter = await accountRepo.findOne({ where: { id: from.id } }); const toAfter = await accountRepo.findOne({ where: { id: to.id } });
expect(fromAfter?.balance).toBe(800); expect(toAfter?.balance).toBe(700); });
it('should rollback on insufficient funds', async () => { // Setup accounts const accountRepo = dataSource.getRepository(Account); const from = await accountRepo.save({ balance: 100 }); const to = await accountRepo.save({ balance: 500 });
// Try transfer await expect( service.transferFunds(from.id, to.id, 200), ).rejects.toThrow('Insufficient funds');
// Verify balances unchanged const fromAfter = await accountRepo.findOne({ where: { id: from.id } }); const toAfter = await accountRepo.findOne({ where: { id: to.id } });
expect(fromAfter?.balance).toBe(100); expect(toAfter?.balance).toBe(500); }); });});42.6 Testing Migrations
Section titled “42.6 Testing Migrations”Migration Integration Test
Section titled “Migration Integration Test”import { DataSource } from 'typeorm';import { TestDatabaseService } from '../test-database.service';
describe('Migrations (Integration)', () => { let dataSource: DataSource;
beforeAll(async () => { // Create fresh database without synchronize dataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'postgres', database: 'myapp_migrations_test', entities: ['src/**/*.entity{.ts,.js}'], migrations: ['src/migrations/*{.ts,.js}'], synchronize: false, dropSchema: true, logging: true, });
await dataSource.initialize(); });
afterAll(async () => { if (dataSource.isInitialized) { await dataSource.destroy(); } });
it('should run all migrations', async () => { const migrations = await dataSource.runMigrations();
expect(migrations.length).toBeGreaterThan(0);
migrations.forEach(m => { console.log(`Executed: ${m.name}`); }); });
it('should have created users table', async () => { const result = await dataSource.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'users' ); `);
expect(result[0].exists).toBe(true); });
it('should have correct columns in users table', async () => { const columns = await dataSource.query(` SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'users' ORDER BY ordinal_position; `);
const columnNames = columns.map((c: any) => c.column_name);
expect(columnNames).toContain('id'); expect(columnNames).toContain('name'); expect(columnNames).toContain('email'); expect(columnNames).toContain('password'); expect(columnNames).toContain('created_at'); });
it('should rollback migrations', async () => { await dataSource.undoLastMigration();
const result = await dataSource.query(` SELECT COUNT(*) FROM migrations; `);
expect(parseInt(result[0].count)).toBe(0); });});42.7 Test Fixtures
Section titled “42.7 Test Fixtures”Fixture Factory
Section titled “Fixture Factory”import { User } from '../../src/users/user.entity';
export class UserFixture { static create(overrides: Partial<User> = {}): User { const user = new User(); user.name = overrides.name || 'Test User'; user.email = overrides.email || `test${Date.now()}@example.com`; user.password = overrides.password || 'hashed_password'; user.isActive = overrides.isActive ?? true; return user; }
static createMany(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, (_, i) => this.create({ ...overrides, email: overrides.email || `test${i}@example.com`, }), ); }}
// test/fixtures/post.fixture.tsimport { Post } from '../../src/posts/post.entity';
export class PostFixture { static create(overrides: Partial<Post> = {}): Post { const post = new Post(); post.title = overrides.title || 'Test Post'; post.content = overrides.content || 'Test content'; post.isPublished = overrides.isPublished ?? true; return post; }
static createForUser(userId: number, overrides: Partial<Post> = {}): Post { const post = this.create(overrides); post.authorId = userId; return post; }}
// Usage in testsdescribe('With Fixtures', () => { it('should use fixtures', async () => { const user = UserFixture.create({ name: 'John Doe' }); const saved = await userRepository.save(user);
expect(saved.name).toBe('John Doe'); });
it('should create multiple users', async () => { const users = UserFixture.createMany(5); const saved = await userRepository.save(users);
expect(saved).toHaveLength(5); });});42.8 Summary
Section titled “42.8 Summary” Integration Testing Quick Reference +------------------------------------------------------------------+ | | | Test Type | Purpose | | -------------------|------------------------------------------| | Repository tests | Test database queries | | Service tests | Test with real database | | Transaction tests | Test atomic operations | | Migration tests | Test schema changes | | | | Setup | Description | | -------------------|------------------------------------------| | Test database | Separate database for tests | | Synchronize | Auto-create schema | | Cleanup | Clear tables between tests | | Fixtures | Reusable test data | | | | Best Practices | Description | | -------------------|------------------------------------------| | Isolated tests | Each test is independent | | Clean state | Clear data before each test | | Realistic data | Use fixtures for consistency | | Longer timeout | Allow time for database operations | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Last Updated: February 2026