Topic 10 of 55
Testing with Jest & Supertest
Overview
Automated testing prevents regressions and enables confident refactoring. Jest is the standard testing framework for Node.js/TypeScript, while Supertest tests HTTP routes end-to-end without needing a running server.
Syntax
javascript
import request from 'supertest';
import { app } from '../src/app';
import { prisma } from '../src/lib/prisma';
// Unit test — test a pure function
describe('calculateTotal', () => {
it('applies discount correctly', () => {
expect(calculateTotal(100, 20)).toBe(80); // 20% off
});
it('never returns negative total', () => {
expect(calculateTotal(100, 120)).toBe(0); // max 100% off
});
});
// Integration test — test route handler
describe('POST /api/products', () => {
beforeEach(async () => {
await prisma.product.deleteMany(); // clean state
});
it('creates a product with valid data', async () => {
const res = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send({ name: 'Laptop', price: 45000, categoryId: 'cat1' });
expect(res.status).toBe(201);
expect(res.body).toMatchObject({ name: 'Laptop', price: 45000 });
expect(res.body.id).toBeDefined();
});
it('returns 400 for invalid data', async () => {
const res = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send({ name: '' }); // invalid
expect(res.status).toBe(400);
expect(res.body.errors).toBeDefined();
});
});Common Pitfalls
- Don't test implementation details — test behavior. If you refactor internals, tests shouldn't break.
- Use a separate test database (TEST_DATABASE_URL) — never run tests against production or development databases.
- Interview tip: The testing pyramid — many unit tests (fast, cheap), some integration tests, few E2E tests (slow, expensive). Aim for 70/20/10 distribution.
Real-World Example
Testing an order creation flow with mocks:
example
javascript
import request from 'supertest';
import { app } from '../src/app';
// Mock external services
jest.mock('../src/lib/payment', () => ({
processPayment: jest.fn().mockResolvedValue({ id: 'pay_123', status: 'success' }),
}));
jest.mock('../src/lib/email', () => ({
sendOrderConfirmation: jest.fn().mockResolvedValue(true),
}));
describe('Order Creation', () => {
let authToken: string;
let productId: string;
beforeAll(async () => {
// Create test user and product
const user = await prisma.user.create({ data: testUser });
authToken = generateToken(user.id);
const product = await prisma.product.create({ data: { ...testProduct, stock: 10 } });
productId = product.id;
});
it('creates order and reduces stock', async () => {
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({ productId, quantity: 2, addressId: 'addr_1' });
expect(res.status).toBe(201);
expect(res.body.status).toBe('confirmed');
// Verify stock was reduced
const product = await prisma.product.findUnique({ where: { id: productId } });
expect(product!.stock).toBe(8); // 10 - 2
// Verify email was sent
expect(sendOrderConfirmation).toHaveBeenCalledWith(
expect.objectContaining({ orderId: res.body.id })
);
});
afterAll(async () => {
await prisma.user.deleteMany();
await prisma.product.deleteMany();
await prisma.$disconnect();
});
});