Appearance
Integration Testing in NestJS
Testing NestJS applications end-to-end with real dependencies
Related: Unit Testing in NestJS | Backend Integration Testing
Overview
Integration tests verify that all parts of your NestJS application work together correctly. They test complete user journeys from HTTP requests through to database persistence, using real implementations rather than mocks.
Testing Philosophy
What to Test
Integration tests should cover:
- Complete API endpoints - From HTTP request to response
- Repository implementations - Against real databases
- Module interactions - Verify modules work together
- Error scenarios - 4xx and 5xx responses
- Business workflows - Multi-step user journeys
Testing Principles
- Use real dependencies - Real database, real modules
- Test through public APIs - HTTP endpoints, not internal methods
- Setup via API calls - Not direct database manipulation
- Clean state between tests - Isolated, repeatable tests
- Test user journeys - Not implementation details
Test Environment Setup
Test Database Configuration
typescript
// test/setup/test-database.ts
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';
import { randomUUID } from 'crypto';
export class TestDatabase {
private prisma: PrismaClient;
private databaseUrl: string;
private schema: string;
constructor() {
// Create unique schema for each test run
this.schema = `test_${randomUUID().replace(/-/g, '_')}`;
this.databaseUrl = this.buildDatabaseUrl();
this.prisma = new PrismaClient({
datasources: {
db: { url: this.databaseUrl },
},
});
}
async setup(): Promise<void> {
// Create schema
await this.prisma.$executeRawUnsafe(
`CREATE SCHEMA IF NOT EXISTS "${this.schema}"`,
);
// Run migrations
execSync('npx prisma migrate deploy', {
env: {
...process.env,
DATABASE_URL: this.databaseUrl,
},
});
}
async teardown(): Promise<void> {
await this.prisma.$disconnect();
// Drop test schema
const adminPrisma = new PrismaClient();
await adminPrisma.$executeRawUnsafe(
`DROP SCHEMA IF EXISTS "${this.schema}" CASCADE`,
);
await adminPrisma.$disconnect();
}
async clear(): Promise<void> {
// Clear all tables but keep schema
const tables = await this.prisma.$queryRaw<Array<{ tablename: string }>>`
SELECT tablename FROM pg_tables
WHERE schemaname = ${this.schema}
`;
for (const { tablename } of tables) {
await this.prisma.$executeRawUnsafe(
`TRUNCATE TABLE "${this.schema}"."${tablename}" CASCADE`,
);
}
}
private buildDatabaseUrl(): string {
const baseUrl = process.env.DATABASE_URL || 'postgresql://localhost/test';
const url = new URL(baseUrl);
url.searchParams.set('schema', this.schema);
return url.toString();
}
}Test Application Factory
typescript
// test/setup/test-app.factory.ts
export class TestAppFactory {
static async create(): Promise<INestApplication> {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(ConfigService)
.useValue(new TestConfigService())
.compile();
const app = moduleFixture.createNestApplication();
// Apply same configuration as production
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new LoggingInterceptor());
await app.init();
return app;
}
}Testing REST Endpoints
Basic Endpoint Testing
typescript
// test/integration/users/create-user.integration.spec.ts
describe('POST /users', () => {
let app: INestApplication;
let database: TestDatabase;
beforeAll(async () => {
database = new TestDatabase();
await database.setup();
app = await TestAppFactory.create();
});
afterAll(async () => {
await app.close();
await database.teardown();
});
beforeEach(async () => {
await database.clear();
});
describe('when creating a new user', () => {
it('should create user successfully', async () => {
const createUserDto = {
email: 'john@example.com',
name: 'John Doe',
password: 'SecurePass123!',
};
const response = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
email: 'john@example.com',
name: 'John Doe',
});
expect(response.body.password).toBeUndefined();
});
it('should return 400 for invalid email', async () => {
const createUserDto = {
email: 'invalid-email',
name: 'John Doe',
password: 'SecurePass123!',
};
const response = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(400);
expect(response.body.message).toContain('email must be an email');
});
it('should return 409 for duplicate email', async () => {
const createUserDto = {
email: 'john@example.com',
name: 'John Doe',
password: 'SecurePass123!',
};
// Create first user
await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(201);
// Attempt to create duplicate
const response = await request(app.getHttpServer())
.post('/users')
.send(createUserDto)
.expect(409);
expect(response.body.message).toBe('User with this email already exists');
});
});
});Testing with Authentication
typescript
describe('Protected endpoints', () => {
let app: INestApplication;
let authToken: string;
let userId: string;
beforeAll(async () => {
app = await TestAppFactory.create();
// Create user and get auth token via API
const { body: user } = await request(app.getHttpServer())
.post('/users')
.send({
email: 'test@example.com',
name: 'Test User',
password: 'TestPass123!',
});
userId = user.id;
const { body: auth } = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'TestPass123!',
});
authToken = auth.accessToken;
});
describe('GET /users/profile', () => {
it('should return user profile when authenticated', async () => {
const response = await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toMatchObject({
id: userId,
email: 'test@example.com',
name: 'Test User',
});
});
it('should return 401 without authentication', async () => {
await request(app.getHttpServer())
.get('/users/profile')
.expect(401);
});
it('should return 401 with invalid token', async () => {
await request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});Testing Business Workflows
Multi-Step User Journeys
typescript
describe('Order workflow', () => {
let app: INestApplication;
let customerToken: string;
let customerId: string;
beforeAll(async () => {
app = await TestAppFactory.create();
// Setup: Create customer
const customer = await createCustomer(app, {
email: 'customer@example.com',
name: 'Test Customer',
});
customerId = customer.id;
customerToken = customer.token;
// Setup: Create products
await createProduct(app, {
id: 'product-1',
name: 'Widget',
price: 10.00,
stock: 100,
});
await createProduct(app, {
id: 'product-2',
name: 'Gadget',
price: 25.00,
stock: 50,
});
});
it('should complete full order workflow', async () => {
// Step 1: Add items to cart
await request(app.getHttpServer())
.post('/cart/items')
.set('Authorization', `Bearer ${customerToken}`)
.send({
productId: 'product-1',
quantity: 2,
})
.expect(201);
await request(app.getHttpServer())
.post('/cart/items')
.set('Authorization', `Bearer ${customerToken}`)
.send({
productId: 'product-2',
quantity: 1,
})
.expect(201);
// Step 2: View cart
const { body: cart } = await request(app.getHttpServer())
.get('/cart')
.set('Authorization', `Bearer ${customerToken}`)
.expect(200);
expect(cart.items).toHaveLength(2);
expect(cart.total).toBe(45.00);
// Step 3: Create order from cart
const { body: order } = await request(app.getHttpServer())
.post('/orders')
.set('Authorization', `Bearer ${customerToken}`)
.send({
shippingAddress: {
street: '123 Main St',
city: 'Anytown',
zipCode: '12345',
},
})
.expect(201);
expect(order.status).toBe('PENDING');
expect(order.total).toBe(45.00);
// Step 4: Process payment
const { body: payment } = await request(app.getHttpServer())
.post(`/orders/${order.id}/payment`)
.set('Authorization', `Bearer ${customerToken}`)
.send({
paymentMethod: 'card',
token: 'test-stripe-token',
})
.expect(200);
expect(payment.status).toBe('SUCCESS');
// Step 5: Verify order status updated
const { body: updatedOrder } = await request(app.getHttpServer())
.get(`/orders/${order.id}`)
.set('Authorization', `Bearer ${customerToken}`)
.expect(200);
expect(updatedOrder.status).toBe('PAID');
// Step 6: Verify inventory was reduced
const { body: product1 } = await request(app.getHttpServer())
.get('/products/product-1')
.expect(200);
expect(product1.stock).toBe(98); // 100 - 2
// Step 7: Verify cart was cleared
const { body: emptyCart } = await request(app.getHttpServer())
.get('/cart')
.set('Authorization', `Bearer ${customerToken}`)
.expect(200);
expect(emptyCart.items).toHaveLength(0);
});
});Testing Repositories
Repository Integration Tests
typescript
describe('UserRepository Integration', () => {
let repository: UserRepository;
let prisma: PrismaService;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [PrismaModule],
providers: [UserRepository],
}).compile();
repository = module.get<UserRepository>(UserRepository);
prisma = module.get<PrismaService>(PrismaService);
});
beforeEach(async () => {
await prisma.user.deleteMany();
});
describe('save', () => {
it('should persist user with all properties', async () => {
const user = User.create(
new Email('test@example.com'),
'Test User',
'password123',
);
await repository.save(user);
const dbRecord = await prisma.user.findUnique({
where: { id: user.getId().toString() },
});
expect(dbRecord).toBeDefined();
expect(dbRecord?.email).toBe('test@example.com');
expect(dbRecord?.name).toBe('Test User');
expect(dbRecord?.status).toBe('ACTIVE');
});
});
describe('findByEmail', () => {
it('should find user by email', async () => {
const user = User.create(
new Email('test@example.com'),
'Test User',
'password123',
);
await repository.save(user);
const found = await repository.findByEmail(
new Email('test@example.com')
);
expect(found).toBeDefined();
expect(found?.getId()).toEqual(user.getId());
});
it('should return null for non-existent email', async () => {
const found = await repository.findByEmail(
new Email('nonexistent@example.com')
);
expect(found).toBeNull();
});
});
describe('transaction support', () => {
it('should rollback on error', async () => {
const user1 = User.create(
new Email('user1@example.com'),
'User 1',
'password123',
);
try {
await prisma.$transaction(async (tx) => {
await repository.save(user1, tx);
// This should fail due to duplicate email
const user2 = User.create(
new Email('user1@example.com'),
'User 2',
'password123',
);
await repository.save(user2, tx);
});
} catch (error) {
// Transaction should have rolled back
}
const count = await prisma.user.count();
expect(count).toBe(0);
});
});
});Test Data Builders
Creating Test Data
typescript
// test/builders/user.builder.ts
export class UserBuilder {
private email = 'test@example.com';
private name = 'Test User';
private password = 'TestPass123!';
private status = UserStatus.ACTIVE;
withEmail(email: string): this {
this.email = email;
return this;
}
withName(name: string): this {
this.name = name;
return this;
}
withStatus(status: UserStatus): this {
this.status = status;
return this;
}
build(): User {
return User.create(
new Email(this.email),
this.name,
this.password,
);
}
async buildAndSave(repository: UserRepositoryInterface): Promise<User> {
const user = this.build();
await repository.save(user);
return user;
}
}
// Usage in tests
const user = await new UserBuilder()
.withEmail('john@example.com')
.withName('John Doe')
.buildAndSave(repository);API Test Helpers
typescript
// test/helpers/api.helpers.ts
export class ApiTestHelper {
constructor(private app: INestApplication) {}
async createUser(data: Partial<CreateUserDto> = {}): Promise<{
user: UserDto;
token: string;
}> {
const createDto = {
email: data.email || `test${Date.now()}@example.com`,
name: data.name || 'Test User',
password: data.password || 'TestPass123!',
};
const { body: user } = await request(this.app.getHttpServer())
.post('/users')
.send(createDto)
.expect(201);
const { body: auth } = await request(this.app.getHttpServer())
.post('/auth/login')
.send({
email: createDto.email,
password: createDto.password,
})
.expect(200);
return { user, token: auth.accessToken };
}
async createOrder(
token: string,
items: OrderItemDto[],
): Promise<OrderDto> {
const { body } = await request(this.app.getHttpServer())
.post('/orders')
.set('Authorization', `Bearer ${token}`)
.send({ items })
.expect(201);
return body;
}
async getAuthenticatedRequest(token: string) {
return request(this.app.getHttpServer())
.set('Authorization', `Bearer ${token}`);
}
}Performance Testing
Load Testing Endpoints
typescript
describe('Performance tests', () => {
let app: INestApplication;
beforeAll(async () => {
app = await TestAppFactory.create();
});
it('should handle concurrent requests', async () => {
const promises = Array.from({ length: 100 }, (_, i) =>
request(app.getHttpServer())
.post('/users')
.send({
email: `user${i}@example.com`,
name: `User ${i}`,
password: 'TestPass123!',
})
);
const results = await Promise.all(promises);
const successCount = results.filter(r => r.status === 201).length;
expect(successCount).toBe(100);
});
it('should complete order workflow within SLA', async () => {
const start = Date.now();
// Complete order workflow
await completeOrderWorkflow(app);
const duration = Date.now() - start;
expect(duration).toBeLessThan(2000); // 2 second SLA
});
});Best Practices
DO ✅
- Test complete user journeys - Not just individual endpoints
- Use real databases - With proper cleanup between tests
- Setup via API calls - Avoid direct database manipulation
- Test error scenarios - 400s, 401s, 404s, 500s
- Verify side effects - Check emails sent, events emitted
DON'T ❌
- Mock dependencies - Use real implementations
- Test implementation details - Focus on behavior
- Share state between tests - Each test should be independent
- Use production database - Always use test database
- Skip database cleanup - Ensures test isolation
Common Patterns
Test Organization
test/
├── integration/
│ ├── users/
│ │ ├── create-user.spec.ts
│ │ ├── update-user.spec.ts
│ │ └── delete-user.spec.ts
│ ├── orders/
│ │ ├── order-workflow.spec.ts
│ │ └── order-cancellation.spec.ts
│ └── repositories/
│ ├── user-repository.spec.ts
│ └── order-repository.spec.ts
├── setup/
│ ├── test-app.factory.ts
│ ├── test-database.ts
│ └── test-config.ts
├── helpers/
│ ├── api.helpers.ts
│ └── database.helpers.ts
└── builders/
├── user.builder.ts
└── order.builder.tsNext Steps
- Learn about Unit Testing in NestJS
- Explore Clean Architecture in NestJS
- Review Repository Pattern for data layer testing
Related Concepts
- Backend Integration Testing - Theoretical foundation
- Test-Driven Development - Testing methodology
- Acceptance Testing - User journey testing