Integration tests verify that multiple pieces, routes, middleware, and (often) a real or test database, work correctly together. This lesson covers testing an Express API's actual HTTP behavior using Supertest.
What Integration Tests Cover
Where unit tests isolate a single function, integration tests exercise a real request going through your actual middleware pipeline, route handlers, and (typically) a real or test database, verifying the pieces work correctly together, not just individually.
Supertest is the standard tool for this in Express apps: it lets you send real HTTP requests directly to your app object in memory, without needing to start an actual network listener, and assert on the response status, headers, and body.
import { test } from 'node:test';
import assert from 'node:assert';
import request from 'supertest';
import app from '../app.js';
test('GET /health returns 200 and status ok', async () => {
const response = await request(app).get('/health');
assert.strictEqual(response.status, 200);
assert.deepStrictEqual(response.body, { status: 'ok' });
});
This is exactly why the earlier Express Basics lesson recommended exporting app separately from listen(), Supertest needs the app object, not a running server.
Integration tests that touch a database should run against a dedicated test database, not production or shared development data, so tests remain repeatable and don't corrupt real data or fail due to unrelated data changes.
Use a separate DATABASE_URL for the test environment (NODE_ENV=test).
Reset or seed the test database before each test run for consistent starting conditions.
Consider an in-memory or containerized database for fully isolated CI runs.
Testing Authentication-Protected Routes
Testing a protected route means obtaining valid credentials first, either by logging in through the API itself within the test, or by directly signing a test JWT, then attaching it to subsequent requests.
Running integration tests against a production or shared development database.
Not resetting test data between test runs, causing tests to interfere with each other.
Forgetting to export the app object separately from listen(), blocking Supertest from working cleanly.
Testing only successful requests and skipping error responses (401, 404, 400) in integration tests.
Key Takeaways
Integration tests verify how routes, middleware, and (often) a database work together.
Supertest sends real HTTP requests to your Express app in memory, without a running network listener.
Use a dedicated test database, reset between runs, for reliable and repeatable results.
Test both success and failure responses, not just the happy path, for protected and unprotected routes alike.
Pro Tip
Keep a small helper function that logs in a test user and returns a valid token, then reuse it across every integration test that needs authentication. It keeps individual test files focused on what they're actually verifying.
You now test both units and full request flows. Next, learn Node.js debugging techniques for diagnosing issues when tests aren't enough.