Skip to content

Integration_testing

Testing TypeORM with Real Database Connections

Section titled “Testing TypeORM with Real Database Connections”

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
================================================================================

test/test-database.config.ts
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.ts
import { 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`);
}
}
}
test/jest-integration.setup.ts
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.js
module.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
};

src/users/user.repository.integration.spec.ts
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');
});
});
});

src/users/users.service.integration.spec.ts
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);
});
});
});

src/orders/orders.service.integration.spec.ts
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);
});
});
});

test/migrations/migrations.integration.spec.ts
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);
});
});

test/fixtures/user.fixture.ts
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.ts
import { 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 tests
describe('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);
});
});

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 |
| |
+------------------------------------------------------------------+

Chapter 43: E2E Testing


Last Updated: February 2026