Skip to main content

Supertest API Integration Testing

Overview​

Supertest is an HTTP assertion library that enables testing of HTTP servers without needing to start a server separately. It's used in the application for API integration tests that verify endpoint behavior, request/response contracts, and database interactions.

What is Supertest?​

Supertest provides:

  • Fluent API: Chain methods for clean, readable tests
  • HTTP Assertions: Verify status codes, headers, and body
  • Automatic Server: No need to manually start/stop server
  • Request Building: Easy multi-part, JSON, form data support
  • Authentication: Handle cookies, bearer tokens, etc.

App Supertest Setup​

Installation​

pnpm add -D supertest @types/supertest

Test Configuration​

apps/api/test/jest-e2e.json​

{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": ["**/*.(t|j)s"]
}

API Integration Testing Examples​

Basic HTTP Tests​

// apps/api/test/employees.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Employees API (E2E)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});

afterAll(async () => {
await app.close();
});

describe('GET /employees', () => {
it('should return list of employees', () => {
return request(app.getHttpServer())
.get('/employees')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body.data)).toBe(true);
});
});

it('should support pagination', () => {
return request(app.getHttpServer())
.get('/employees')
.query({ page: 1, limit: 10 })
.expect(200)
.expect((res) => {
expect(res.body.meta.page).toBe(1);
expect(res.body.meta.limit).toBe(10);
});
});

it('should filter by department', () => {
return request(app.getHttpServer())
.get('/employees')
.query({ department: 'Engineering' })
.expect(200)
.expect((res) => {
res.body.data.forEach((emp) => {
expect(emp.department).toBe('Engineering');
});
});
});
});

describe('POST /employees', () => {
it('should create employee with valid data', () => {
const newEmployee = {
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
};

return request(app.getHttpServer())
.post('/employees')
.send(newEmployee)
.expect(201)
.expect((res) => {
expect(res.body.data.id).toBeDefined();
expect(res.body.data.name).toBe('John Doe');
expect(res.body.data.email).toBe('john@example.com');
});
});

it('should return 400 for missing required fields', () => {
return request(app.getHttpServer())
.post('/employees')
.send({ name: 'John' }) // missing email and department
.expect(400)
.expect((res) => {
expect(res.body.error).toBeDefined();
expect(res.body.error.message).toContain('email');
});
});

it('should return 409 for duplicate email', () => {
const payload = {
name: 'Jane Doe',
email: 'jane@example.com',
department: 'Sales',
};

return request(app.getHttpServer())
.post('/employees')
.send(payload)
.then(() =>
request(app.getHttpServer())
.post('/employees')
.send({
...payload,
name: 'Different Name',
})
.expect(409)
.expect((res) => {
expect(res.body.error.code).toBe('DUPLICATE_EMAIL');
}),
);
});

it('should validate email format', () => {
return request(app.getHttpServer())
.post('/employees')
.send({
name: 'John',
email: 'invalid-email',
department: 'Engineering',
})
.expect(400)
.expect((res) => {
expect(res.body.error.details).toContainEqual(
expect.objectContaining({
field: 'email',
message: 'Invalid email format',
}),
);
});
});
});

describe('GET /employees/:id', () => {
let employeeId: string;

beforeAll(async () => {
const res = await request(app.getHttpServer()).post('/employees').send({
name: 'Test Employee',
email: 'test@example.com',
department: 'Engineering',
});
employeeId = res.body.data.id;
});

it('should return employee by id', () => {
return request(app.getHttpServer())
.get(`/employees/${employeeId}`)
.expect(200)
.expect((res) => {
expect(res.body.data.id).toBe(employeeId);
expect(res.body.data.name).toBe('Test Employee');
});
});

it('should return 404 for non-existent employee', () => {
return request(app.getHttpServer())
.get('/employees/invalid-id')
.expect(404)
.expect((res) => {
expect(res.body.error.code).toBe('NOT_FOUND');
});
});
});

describe('PATCH /employees/:id', () => {
let employeeId: string;

beforeAll(async () => {
const res = await request(app.getHttpServer()).post('/employees').send({
name: 'Original Name',
email: 'original@example.com',
department: 'Engineering',
});
employeeId = res.body.data.id;
});

it('should update employee', () => {
return request(app.getHttpServer())
.patch(`/employees/${employeeId}`)
.send({ department: 'Management' })
.expect(200)
.expect((res) => {
expect(res.body.data.department).toBe('Management');
expect(res.body.data.name).toBe('Original Name'); // unchanged
});
});

it('should validate updated fields', () => {
return request(app.getHttpServer())
.patch(`/employees/${employeeId}`)
.send({ email: 'invalid-email' })
.expect(400);
});
});

describe('DELETE /employees/:id', () => {
let employeeId: string;

beforeAll(async () => {
const res = await request(app.getHttpServer()).post('/employees').send({
name: 'To Delete',
email: 'delete@example.com',
department: 'Engineering',
});
employeeId = res.body.data.id;
});

it('should delete employee', () => {
return request(app.getHttpServer()).delete(`/employees/${employeeId}`).expect(204);
});

it('should return 404 after deletion', () => {
return request(app.getHttpServer()).get(`/employees/${employeeId}`).expect(404);
});
});
});

Authentication Testing​

Bearer Token Authentication​

describe('Protected Endpoints', () => {
let accessToken: string;

beforeAll(async () => {
// Get access token
const res = await request(app.getHttpServer()).post('/auth/login').send({
email: 'user@example.com',
password: 'password123',
});
accessToken = res.body.data.accessToken;
});

it('should reject without token', () => {
return request(app.getHttpServer())
.get('/profile')
.expect(401)
.expect((res) => {
expect(res.body.error.code).toBe('UNAUTHORIZED');
});
});

it('should accept valid token', () => {
return request(app.getHttpServer())
.get('/profile')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
});

it('should reject invalid token', () => {
return request(app.getHttpServer())
.get('/profile')
.set('Authorization', 'Bearer invalid_token')
.expect(401);
});

it('should reject expired token', () => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // expired JWT

return request(app.getHttpServer())
.get('/profile')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
});
});

File Upload Testing​

describe('File Upload', () => {
it('should upload employee document', () => {
return request(app.getHttpServer())
.post('/employees/1/documents')
.set('Authorization', `Bearer ${accessToken}`)
.field('name', 'Resume')
.attach('file', '/path/to/resume.pdf')
.expect(201)
.expect((res) => {
expect(res.body.data.fileId).toBeDefined();
expect(res.body.data.fileName).toBe('resume.pdf');
});
});

it('should reject files larger than 10MB', () => {
return request(app.getHttpServer())
.post('/employees/1/documents')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', '/path/to/large-file.zip') // > 10MB
.expect(413);
});
});

Request/Response Validation​

Header Testing​

it('should return proper headers', () => {
return request(app.getHttpServer())
.get('/employees')
.expect(200)
.expect((res) => {
expect(res.headers['content-type']).toContain('application/json');
expect(res.headers['x-request-id']).toBeDefined();
expect(res.headers['cache-control']).toBe('no-cache');
});
});
it('should set secure cookies', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'user@example.com', password: 'pass' })
.expect(200)
.expect((res) => {
expect(res.headers['set-cookie']).toBeDefined();
const cookies = res.headers['set-cookie'];
expect(cookies).toEqual(
expect.arrayContaining([
expect.stringContaining('HttpOnly'),
expect.stringContaining('Secure'),
]),
);
});
});

Database State Management​

Setup and Teardown​

describe('Employee Database Operations', () => {
const prisma = new PrismaClient();

beforeAll(async () => {
// Run migrations
await prisma.$executeRawUnsafe('DELETE FROM user_session; DELETE FROM employees;');
});

afterEach(async () => {
// Clean up after each test
await prisma.employee.deleteMany({});
});

afterAll(async () => {
await prisma.$disconnect();
});

it('should create employee in database', async () => {
await request(app.getHttpServer())
.post('/employees')
.send({
name: 'Database Test',
email: 'dbtest@example.com',
department: 'Engineering',
})
.expect(201);

// Verify in database
const employee = await prisma.employee.findUnique({
where: { email: 'dbtest@example.com' },
});

expect(employee).toBeDefined();
expect(employee.name).toBe('Database Test');
});
});

Response Chaining and Assertions​

it('should handle complex workflows', () => {
return request(app.getHttpServer())
.post('/employees')
.send({
name: 'John',
email: 'john@example.com',
department: 'Engineering',
})
.then((createRes) => {
const employeeId = createRes.body.data.id;

return request(app.getHttpServer())
.get(`/employees/${employeeId}`)
.expect(200)
.then((getRes) => {
expect(getRes.body.data.id).toBe(employeeId);
expect(getRes.body.data.email).toBe('john@example.com');
});
});
});

Error Response Testing​

describe('Error Handling', () => {
it('should return standardized error format', () => {
return request(app.getHttpServer())
.post('/employees')
.send({}) // Missing required fields
.expect(400)
.expect((res) => {
expect(res.body).toHaveProperty('error');
expect(res.body.error).toHaveProperty('code');
expect(res.body.error).toHaveProperty('message');
expect(res.body.error).toHaveProperty('details');
});
});

it('should handle server errors gracefully', () => {
// Simulate database error
jest.spyOn(prisma.employee, 'findMany').mockRejectedValue(new Error('Database error'));

return request(app.getHttpServer())
.get('/employees')
.expect(500)
.expect((res) => {
expect(res.body.error.code).toBe('INTERNAL_SERVER_ERROR');
});
});
});

Running Integration Tests​

# Run all integration tests
pnpm --filter qms-api test:e2e

# Run specific test file
pnpm --filter qms-api test:e2e employees.e2e-spec.ts

# Run with coverage
pnpm --filter qms-api test:e2e -- --coverage

# Watch mode
pnpm --filter qms-api test:e2e -- --watch

Best Practices​

1. Test Realistic Scenarios​

// βœ… GOOD: Tests real user workflows
describe('User Registration Flow', () => {
it('should complete full registration', async () => {
// Sign up
const signUpRes = await request(app.getHttpServer()).post('/auth/signup').send(/* ... */);

const token = signUpRes.body.data.token;

// Verify email
await request(app.getHttpServer())
.post('/auth/verify-email')
.set('Authorization', `Bearer ${token}`)
.send(/* ... */);

// Set up profile
await request(app.getHttpServer())
.post('/profile')
.set('Authorization', `Bearer ${token}`)
.send(/* ... */);
});
});

2. Use Test Fixtures​

// Create reusable test data
const createTestEmployee = async () => {
const res = await request(app.getHttpServer())
.post('/employees')
.send({
name: 'Test Employee',
email: `test-${Date.now()}@example.com`,
department: 'Engineering',
});
return res.body.data;
};

// Use in tests
let testEmployee;
beforeEach(async () => {
testEmployee = await createTestEmployee();
});

3. Verify Side Effects​

it('should trigger email notification', async () => {
const emailSpy = jest.spyOn(emailService, 'send');

await request(app.getHttpServer()).post('/employees').send(/* ... */);

expect(emailSpy).toHaveBeenCalledWith(
expect.objectContaining({
to: 'john@example.com',
subject: expect.stringContaining('Welcome'),
}),
);
});

4. Test Concurrency and Race Conditions​

it('should handle concurrent requests', async () => {
const promises = Array(10)
.fill(null)
.map((_, i) =>
request(app.getHttpServer())
.post('/employees')
.send({
name: `Employee ${i}`,
email: `employee${i}@example.com`,
department: 'Engineering',
}),
);

await Promise.all(promises);

const employees = await prisma.employee.findMany();
expect(employees).toHaveLength(10);
});

Resources​