Integration & API Testing
📖 Concept
Integration tests verify that multiple components work together correctly. For Node.js APIs, this means testing the full request → middleware → route → controller → service → database flow.
Supertest is the standard library for HTTP integration testing in Node.js. It works by importing your Express app and making real HTTP requests against it — without starting a server.
What to test in integration tests:
- HTTP status codes for all routes
- Response body structure and content
- Request validation (invalid input returns 400)
- Authentication and authorization
- Database side effects (records created/updated)
- Error handling (404, 500)
Test database strategies:
| Strategy | Pros | Cons |
|---|---|---|
| In-memory DB (SQLite) | Fast, no setup | Different behavior from production |
| Docker container | Same DB as production | Slower, needs Docker |
| Test database | Real database | Needs cleanup between tests |
| Mocked DB layer | Fastest | Doesn't test real queries |
🏠 Real-world analogy: If unit tests inspect individual car parts, integration tests take the assembled car for a test drive — checking that the engine, transmission, and wheels work together. API testing is like sending the car through an automated inspection course — it must pass every station (endpoint) to be approved.
💻 Code Example
1// Integration Testing with Supertest23const request = require("supertest");4const app = require("../../src/app"); // Import app WITHOUT server.listen()56describe("POST /api/users", () => {7 test("should create a user with valid data", async () => {8 const res = await request(app)9 .post("/api/users")10 .send({ name: "Alice", email: "alice@test.com", password: "secure123" })11 .expect(201);1213 expect(res.body.success).toBe(true);14 expect(res.body.data).toMatchObject({15 name: "Alice",16 email: "alice@test.com",17 });18 expect(res.body.data.password).toBeUndefined(); // Not exposed19 expect(res.body.data.id).toBeDefined();20 });2122 test("should return 400 for missing required fields", async () => {23 const res = await request(app)24 .post("/api/users")25 .send({ name: "Alice" }) // Missing email26 .expect(400);2728 expect(res.body.success).toBe(false);29 expect(res.body.error).toBeDefined();30 });3132 test("should return 409 for duplicate email", async () => {33 // First create34 await request(app)35 .post("/api/users")36 .send({ name: "Alice", email: "dup@test.com", password: "secure123" });3738 // Duplicate39 const res = await request(app)40 .post("/api/users")41 .send({ name: "Bob", email: "dup@test.com", password: "secure123" })42 .expect(409);4344 expect(res.body.error.message).toContain("already");45 });46});4748describe("GET /api/users", () => {49 test("should return paginated users", async () => {50 const res = await request(app)51 .get("/api/users?page=1&limit=10")52 .expect(200);5354 expect(res.body.data).toBeInstanceOf(Array);55 expect(res.body.meta).toMatchObject({56 page: 1,57 limit: 10,58 total: expect.any(Number),59 });60 });6162 test("should filter by role", async () => {63 const res = await request(app)64 .get("/api/users?role=admin")65 .expect(200);6667 res.body.data.forEach((user) => {68 expect(user.role).toBe("admin");69 });70 });71});7273describe("Protected routes", () => {74 let authToken;7576 beforeAll(async () => {77 // Login to get token78 const res = await request(app)79 .post("/api/auth/login")80 .send({ email: "admin@test.com", password: "admin123" });81 authToken = res.body.accessToken;82 });8384 test("should return 401 without token", async () => {85 await request(app).get("/api/profile").expect(401);86 });8788 test("should return profile with valid token", async () => {89 const res = await request(app)90 .get("/api/profile")91 .set("Authorization", `Bearer ${authToken}`)92 .expect(200);9394 expect(res.body.data.email).toBeDefined();95 });9697 test("should return 403 for unauthorized role", async () => {98 await request(app)99 .get("/api/admin/dashboard")100 .set("Authorization", `Bearer ${authToken}`)101 .expect(403);102 });103});104105describe("Error handling", () => {106 test("should return 404 for unknown routes", async () => {107 const res = await request(app).get("/api/nonexistent").expect(404);108 expect(res.body.success).toBe(false);109 });110111 test("should return 500 for server errors", async () => {112 // Trigger an internal error (e.g., bad query)113 const res = await request(app)114 .get("/api/crash-test")115 .expect(500);116117 expect(res.body.error).not.toContain("stack"); // No stack in production118 });119});
🏋️ Practice Exercise
Exercises:
- Write integration tests for all CRUD endpoints of a user API using Supertest
- Test authentication — verify login returns a token, protected routes reject without it
- Test pagination, filtering, and sorting query parameters
- Set up a test database that is reset before each test suite
- Test error responses — 400 (validation), 401 (auth), 403 (forbidden), 404, 500
- Generate a test coverage report and achieve 80%+ coverage across all layers
⚠️ Common Mistakes
Starting the server in test files — import
app(Express instance), notserver(with .listen()); Supertest handles the HTTP internallyNot cleaning up test data between tests — use
beforeEachto reset the database or seed known stateTesting only the happy path — integration tests must cover error cases, edge cases, and auth failures
Making tests dependent on each other — each test should create its own data; don't rely on data from a previous test
Using the same database for development and testing — use a separate test database or in-memory DB to avoid data conflicts
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Integration & API Testing. Login to unlock this feature.