Integration tests with Supertest
You met Supertest briefly when you built the health check. The same library scales all the way to full integration tests. You hand it the Express app and it simulates real HTTP calls in memory. Zero port binding, zero flake.
import { describe, it, expect, beforeAll, beforeEach, afterAll } from '@jest/globals';
import supertest from 'supertest';
let app: any;
let request: any;
let adminToken: string;
beforeAll(async () => {
// app setup happens here, including the test database
const mod = await import('../after/index.js');
app = mod.app;
request = supertest(app);
await request.post('/api/auth/register').send({ username: 'admin', password: 'password123' });
// promote to admin, then log in to get a token
});
describe('Secured book endpoints', () => {
it('lets anyone read books', async () => {
const res = await request.get('/api/books');
expect(res.status).toBe(200);
});
it('blocks anonymous writes', async () => {
const res = await request.post('/api/books').send({ title: 'X', author: 'Y', pages: 1, published: '2024' });
expect(res.status).toBe(401);
});
it('lets admins create books', async () => {
const res = await request
.post('/api/books')
.set('Authorization', 'Bearer ' + adminToken)
.send({ title: 'X', author: 'Y', pages: 100, published: '2024' });
expect(res.status).toBe(201);
});
});A real integration test against the auth and book routes. Notice the beforeAll/beforeEach/afterAll lifecycle.
The lifecycle hooks matter. beforeAll runs once and does expensive setup: starting the database, creating test users. beforeEach runs before every test and resets shared state so cases do not bleed into each other. afterAll runs once at the end to close connections and tear everything down. Use them in exactly that shape and your tests stay fast and isolated.
Quiz: Quiz
Loading practiceโฆ