Skip to content

E2e_testing

End-to-End Testing with TypeORM and NestJS

Section titled “End-to-End Testing with TypeORM and NestJS”

End-to-end tests verify the entire application stack from HTTP request to database response.

E2E Testing Flow
================================================================================
HTTP Request
|
v
+------------------+
| Test Client | <-- supertest
+------------------+
|
v
+------------------+
| NestJS App | <-- Full application
+------------------+
|
v
+------------------+
| Service |
+------------------+
|
v
+------------------+
| Database | <-- Test database
+------------------+
What E2E Tests Verify:
- HTTP endpoints
- Request/response format
- Authentication
- Authorization
- Full request flow
================================================================================

test/test-app.factory.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';
import { ValidationPipe } from '@nestjs/common';
import { DataSource } from 'typeorm';
export class TestAppFactory {
private static app: any;
private static dataSource: DataSource;
static async create() {
const module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(DataSource)
.useFactory({
factory: async () => {
this.dataSource = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'myapp_e2e_test',
entities: ['src/**/*.entity{.ts,.js}'],
synchronize: true,
dropSchema: true,
});
return this.dataSource.initialize();
},
})
.compile();
this.app = module.createNestApplication();
// Apply same configuration as production
this.app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await this.app.init();
return this.app;
}
static async cleanup() {
if (this.dataSource?.isInitialized) {
await this.dataSource.destroy();
}
if (this.app) {
await this.app.close();
}
}
static async clearDatabase() {
if (this.dataSource?.isInitialized) {
const entities = this.dataSource.entityMetadatas;
for (const entity of entities) {
const repository = this.dataSource.getRepository(entity.name);
await repository.query(`TRUNCATE "${entity.tableName}" CASCADE`);
}
}
}
}
test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TestDatabaseService } from './test-database.service';
describe('Users (e2e)', () => {
let app: INestApplication;
let accessToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(DataSource)
.useFactory({
factory: async () => {
return TestDatabaseService.getDataSource();
},
})
.compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
await TestDatabaseService.cleanup();
});
beforeEach(async () => {
await TestDatabaseService.clearTables();
});
describe('/users (POST)', () => {
it('should create a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
})
.expect(201)
.expect((res) => {
expect(res.body).toMatchObject({
name: 'John Doe',
email: 'john@example.com',
});
expect(res.body.id).toBeDefined();
expect(res.body.password).toBeUndefined();
});
});
it('should reject invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.send({
name: 'John Doe',
email: 'invalid-email',
password: 'password123',
})
.expect(400);
});
it('should reject short password', () => {
return request(app.getHttpServer())
.post('/users')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'short',
})
.expect(400);
});
});
describe('/users (GET)', () => {
it('should return empty array initially', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect([]);
});
it('should return created users', async () => {
// Create user first
await request(app.getHttpServer())
.post('/users')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
});
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(res.body).toHaveLength(1);
expect(res.body[0].name).toBe('John Doe');
});
});
});
describe('/users/:id (GET)', () => {
it('should return a user by id', async () => {
const createRes = await request(app.getHttpServer())
.post('/users')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
});
const userId = createRes.body.id;
return request(app.getHttpServer())
.get(`/users/${userId}`)
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('John Doe');
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/999')
.expect(404);
});
});
});

test/auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TestDatabaseService } from './test-database.service';
import * as bcrypt from 'bcrypt';
describe('Authentication (e2e)', () => {
let app: INestApplication;
let accessToken: string;
let refreshToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await TestDatabaseService.clearTables();
});
describe('/auth/register (POST)', () => {
it('should register a new user', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
})
.expect(201)
.expect((res) => {
expect(res.body).toMatchObject({
user: {
name: 'John Doe',
email: 'john@example.com',
},
accessToken: expect.any(String),
refreshToken: expect.any(String),
});
});
});
it('should reject duplicate email', async () => {
await request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
});
return request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'Jane Doe',
email: 'john@example.com',
password: 'password456',
})
.expect(409);
});
});
describe('/auth/login (POST)', () => {
beforeEach(async () => {
await request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
});
});
it('should login with correct credentials', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'john@example.com',
password: 'password123',
})
.expect(200)
.expect((res) => {
expect(res.body.accessToken).toBeDefined();
expect(res.body.refreshToken).toBeDefined();
accessToken = res.body.accessToken;
refreshToken = res.body.refreshToken;
});
});
it('should reject wrong password', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'john@example.com',
password: 'wrongpassword',
})
.expect(401);
});
it('should reject non-existent user', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123',
})
.expect(401);
});
});
describe('/auth/refresh (POST)', () => {
beforeEach(async () => {
const res = await request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
});
refreshToken = res.body.refreshToken;
});
it('should refresh tokens', () => {
return request(app.getHttpServer())
.post('/auth/refresh')
.send({ refreshToken })
.expect(200)
.expect((res) => {
expect(res.body.accessToken).toBeDefined();
expect(res.body.refreshToken).toBeDefined();
});
});
it('should reject invalid refresh token', () => {
return request(app.getHttpServer())
.post('/auth/refresh')
.send({ refreshToken: 'invalid-token' })
.expect(401);
});
});
describe('Protected Routes', () => {
beforeEach(async () => {
const res = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'john@example.com',
password: 'password123',
});
accessToken = res.body.accessToken;
});
it('should access protected route with token', () => {
return request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
});
it('should reject without token', () => {
return request(app.getHttpServer())
.get('/users/profile')
.expect(401);
});
it('should reject invalid token', () => {
return request(app.getHttpServer())
.get('/users/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});

test/upload.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import * as path from 'path';
import * as fs from 'fs';
describe('File Upload (e2e)', () => {
let app: INestApplication;
let accessToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users/:id/avatar (POST)', () => {
it('should upload avatar', async () => {
const testImagePath = path.join(__dirname, 'fixtures', 'test-avatar.png');
// Create test image if not exists
if (!fs.existsSync(testImagePath)) {
fs.mkdirSync(path.dirname(testImagePath), { recursive: true });
fs.writeFileSync(testImagePath, Buffer.from([0x89, 0x50, 0x4E, 0x47]));
}
return request(app.getHttpServer())
.post('/users/1/avatar')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', testImagePath)
.expect(201)
.expect((res) => {
expect(res.body.avatarUrl).toBeDefined();
});
});
it('should reject non-image file', () => {
const testFilePath = path.join(__dirname, 'fixtures', 'test.txt');
fs.writeFileSync(testFilePath, 'test content');
return request(app.getHttpServer())
.post('/users/1/avatar')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', testFilePath)
.expect(400);
});
it('should reject large file', () => {
// Create a file larger than limit
const largeFile = path.join(__dirname, 'fixtures', 'large.bin');
const buffer = Buffer.alloc(10 * 1024 * 1024); // 10MB
fs.writeFileSync(largeFile, buffer);
return request(app.getHttpServer())
.post('/users/1/avatar')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', largeFile)
.expect(400);
});
});
});

test/users-query.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { TestDatabaseService } from './test-database.service';
import { UserFixture } from './fixtures/user.fixture';
describe('Users Query (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await TestDatabaseService.clearTables();
});
describe('Pagination', () => {
beforeEach(async () => {
// Create 25 users
const users = UserFixture.createMany(25);
await request(app.getHttpServer())
.post('/users/bulk')
.send(users);
});
it('should return first page', () => {
return request(app.getHttpServer())
.get('/users?page=1&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(10);
expect(res.body.meta.total).toBe(25);
expect(res.body.meta.page).toBe(1);
expect(res.body.meta.totalPages).toBe(3);
});
});
it('should return second page', () => {
return request(app.getHttpServer())
.get('/users?page=2&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(10);
expect(res.body.meta.page).toBe(2);
});
});
it('should return last page', () => {
return request(app.getHttpServer())
.get('/users?page=3&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(5);
});
});
it('should handle invalid page', () => {
return request(app.getHttpServer())
.get('/users?page=999&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(0);
});
});
});
describe('Filtering', () => {
beforeEach(async () => {
await request(app.getHttpServer())
.post('/users')
.send({ name: 'John Doe', email: 'john@example.com', isActive: true });
await request(app.getHttpServer())
.post('/users')
.send({ name: 'Jane Doe', email: 'jane@example.com', isActive: false });
await request(app.getHttpServer())
.post('/users')
.send({ name: 'Bob Smith', email: 'bob@example.com', isActive: true });
});
it('should filter by active status', () => {
return request(app.getHttpServer())
.get('/users?isActive=true')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(2);
res.body.data.forEach((user: any) => {
expect(user.isActive).toBe(true);
});
});
});
it('should search by name', () => {
return request(app.getHttpServer())
.get('/users?search=Doe')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(2);
res.body.data.forEach((user: any) => {
expect(user.name).toContain('Doe');
});
});
});
it('should combine filters', () => {
return request(app.getHttpServer())
.get('/users?isActive=true&search=John')
.expect(200)
.expect((res) => {
expect(res.body.data).toHaveLength(1);
expect(res.body.data[0].name).toBe('John Doe');
});
});
});
describe('Sorting', () => {
beforeEach(async () => {
await request(app.getHttpServer())
.post('/users')
.send({ name: 'Zack', email: 'zack@example.com' });
await request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' });
await request(app.getHttpServer())
.post('/users')
.send({ name: 'Bob', email: 'bob@example.com' });
});
it('should sort by name ascending', () => {
return request(app.getHttpServer())
.get('/users?sortBy=name&order=asc')
.expect(200)
.expect((res) => {
expect(res.body.data[0].name).toBe('Alice');
expect(res.body.data[1].name).toBe('Bob');
expect(res.body.data[2].name).toBe('Zack');
});
});
it('should sort by name descending', () => {
return request(app.getHttpServer())
.get('/users?sortBy=name&order=desc')
.expect(200)
.expect((res) => {
expect(res.body.data[0].name).toBe('Zack');
expect(res.body.data[1].name).toBe('Bob');
expect(res.body.data[2].name).toBe('Alice');
});
});
});
});

test/helpers/auth.helper.ts
import * as request from 'supertest';
export class AuthHelper {
constructor(private app: any) {}
async register(userData: any): Promise<{ accessToken: string; userId: number }> {
const res = await request(this.app.getHttpServer())
.post('/auth/register')
.send(userData);
return {
accessToken: res.body.accessToken,
userId: res.body.user.id,
};
}
async login(email: string, password: string): Promise<string> {
const res = await request(this.app.getHttpServer())
.post('/auth/login')
.send({ email, password });
return res.body.accessToken;
}
authenticatedRequest(accessToken: string) {
return {
get: (url: string) =>
request(this.app.getHttpServer())
.get(url)
.set('Authorization', `Bearer ${accessToken}`),
post: (url: string) =>
request(this.app.getHttpServer())
.post(url)
.set('Authorization', `Bearer ${accessToken}`),
put: (url: string) =>
request(this.app.getHttpServer())
.put(url)
.set('Authorization', `Bearer ${accessToken}`),
delete: (url: string) =>
request(this.app.getHttpServer())
.delete(url)
.set('Authorization', `Bearer ${accessToken}`),
};
}
}
// Usage
describe('With Auth Helper', () => {
let authHelper: AuthHelper;
let accessToken: string;
beforeEach(async () => {
authHelper = new AuthHelper(app);
const result = await authHelper.register({
name: 'Test User',
email: 'test@example.com',
password: 'password123',
});
accessToken = result.accessToken;
});
it('should access protected route', async () => {
const { body } = await authHelper
.authenticatedRequest(accessToken)
.get('/users/profile')
.expect(200);
expect(body.email).toBe('test@example.com');
});
});

E2E Testing Quick Reference
+------------------------------------------------------------------+
| |
| Test Type | Purpose |
| -------------------|------------------------------------------|
| Endpoint tests | Test HTTP endpoints |
| Auth tests | Test authentication flow |
| Upload tests | Test file uploads |
| Query tests | Test pagination, filtering, sorting |
| |
| Tools | Description |
| -------------------|------------------------------------------|
| supertest | HTTP request testing |
| Test factories | Create test applications |
| Fixtures | Reusable test data |
| Helpers | Common test operations |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Clean database | Reset state between tests |
| Test isolation | Each test is independent |
| Realistic data | Use fixtures for consistency |
| Test auth | Include authentication in tests |
| |
+------------------------------------------------------------------+

Chapter 44: Test Fixtures & Factories


Last Updated: February 2026