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

codeTap to expand ⛶
1// Integration Testing with Supertest
2
3const request = require("supertest");
4const app = require("../../src/app"); // Import app WITHOUT server.listen()
5
6describe("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);
12
13 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 exposed
19 expect(res.body.data.id).toBeDefined();
20 });
21
22 test("should return 400 for missing required fields", async () => {
23 const res = await request(app)
24 .post("/api/users")
25 .send({ name: "Alice" }) // Missing email
26 .expect(400);
27
28 expect(res.body.success).toBe(false);
29 expect(res.body.error).toBeDefined();
30 });
31
32 test("should return 409 for duplicate email", async () => {
33 // First create
34 await request(app)
35 .post("/api/users")
36 .send({ name: "Alice", email: "dup@test.com", password: "secure123" });
37
38 // Duplicate
39 const res = await request(app)
40 .post("/api/users")
41 .send({ name: "Bob", email: "dup@test.com", password: "secure123" })
42 .expect(409);
43
44 expect(res.body.error.message).toContain("already");
45 });
46});
47
48describe("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);
53
54 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 });
61
62 test("should filter by role", async () => {
63 const res = await request(app)
64 .get("/api/users?role=admin")
65 .expect(200);
66
67 res.body.data.forEach((user) => {
68 expect(user.role).toBe("admin");
69 });
70 });
71});
72
73describe("Protected routes", () => {
74 let authToken;
75
76 beforeAll(async () => {
77 // Login to get token
78 const res = await request(app)
79 .post("/api/auth/login")
80 .send({ email: "admin@test.com", password: "admin123" });
81 authToken = res.body.accessToken;
82 });
83
84 test("should return 401 without token", async () => {
85 await request(app).get("/api/profile").expect(401);
86 });
87
88 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);
93
94 expect(res.body.data.email).toBeDefined();
95 });
96
97 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});
104
105describe("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 });
110
111 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);
116
117 expect(res.body.error).not.toContain("stack"); // No stack in production
118 });
119});

🏋️ Practice Exercise

Exercises:

  1. Write integration tests for all CRUD endpoints of a user API using Supertest
  2. Test authentication — verify login returns a token, protected routes reject without it
  3. Test pagination, filtering, and sorting query parameters
  4. Set up a test database that is reset before each test suite
  5. Test error responses — 400 (validation), 401 (auth), 403 (forbidden), 404, 500
  6. Generate a test coverage report and achieve 80%+ coverage across all layers

⚠️ Common Mistakes

  • Starting the server in test files — import app (Express instance), not server (with .listen()); Supertest handles the HTTP internally

  • Not cleaning up test data between tests — use beforeEach to reset the database or seed known state

  • Testing 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.