🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

How to Write Integration Tests for a REST API in 2026: Complete Guide

⏱️5 min read  ·  937 words

Integration tests verify that your API’s pieces work together correctly — routes, database, and business logic — by making real requests and checking real responses. Unlike unit tests (which mock everything), integration tests catch bugs at the boundaries. This guide shows how to write them well.

Unit vs Integration Tests

Aspect Unit Test Integration Test
Scope One function in isolation Multiple parts together (route + DB)
Dependencies Mocked Real (or test) database
Speed Very fast Slower (real I/O)
Catches Logic bugs Integration/wiring bugs

You want both — unit tests for logic, integration tests to verify the whole request-response flow works.

Node.js: Testing with Supertest

npm install --save-dev jest supertest
// Export your app WITHOUT calling listen() so tests can use it
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', getUser);
app.post('/users', createUser);
module.exports = app;   // export the app

// server.js (separate) starts it
const app = require('./app');
app.listen(3000);
// users.test.js
const request = require('supertest');
const app = require('./app');
const db = require('./db');

describe('Users API', () => {
  beforeEach(async () => {
    await db.reset();   // clean database before each test
  });

  afterAll(async () => {
    await db.close();
  });

  it('creates a user', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ name: 'Alice' });
    expect(res.body.id).toBeDefined();
  });

  it('returns 404 for missing user', async () => {
    const res = await request(app).get('/users/999');
    expect(res.status).toBe(404);
  });

  it('validates required fields', async () => {
    const res = await request(app).post('/users').send({});
    expect(res.status).toBe(400);
    expect(res.body.error).toBeDefined();
  });
});

Python: Testing with pytest and TestClient

pip install pytest httpx
# test_users.py (FastAPI example)
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_test_db

client = TestClient(app)

@pytest.fixture(autouse=True)
def reset_db():
    db = get_test_db()
    db.reset()
    yield
    db.close()

def test_create_user():
    response = client.post("/users", json={
        "name": "Alice", "email": "alice@example.com"
    })
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Alice"
    assert "id" in data

def test_get_missing_user():
    response = client.get("/users/999")
    assert response.status_code == 404

def test_validation_error():
    response = client.post("/users", json={})
    assert response.status_code == 422

Using a Test Database

Integration tests need a real (test) database — separate from development and production:

# Spin up a test database with Docker
docker run -d --name test-db -p 5433:5432 \
  -e POSTGRES_DB=testdb -e POSTGRES_PASSWORD=test \
  postgres:15-alpine

# Point tests at it via environment
DATABASE_URL=postgresql://postgres:test@localhost:5433/testdb
// Fast reset using transactions (rollback after each test)
beforeEach(async () => {
  await db.query('BEGIN');
});
afterEach(async () => {
  await db.query('ROLLBACK');   // undo all changes — fast and clean
});

Testing Authenticated Endpoints

describe('Protected routes', () => {
  let token;

  beforeAll(async () => {
    const res = await request(app)
      .post('/login')
      .send({ email: 'test@example.com', password: 'password' });
    token = res.body.accessToken;
  });

  it('allows access with valid token', async () => {
    const res = await request(app)
      .get('/protected')
      .set('Authorization', `Bearer ${token}`);
    expect(res.status).toBe(200);
  });

  it('rejects without token', async () => {
    const res = await request(app).get('/protected');
    expect(res.status).toBe(401);
  });
});

Running Integration Tests in CI

# GitHub Actions with a service database
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: testdb
          POSTGRES_PASSWORD: test
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb

Best Practices

  • Isolate tests: Each test should be independent — reset the database between tests
  • Test the contract: Verify status codes, response shape, and error cases
  • Cover error paths: Test 400s, 401s, 404s, and validation failures, not just the happy path
  • Use a real test database: Don’t mock the database in integration tests
  • Keep them reasonably fast: Transaction rollback is faster than truncating tables
  • Run in CI: Integration tests catch wiring bugs that unit tests miss

Frequently Asked Questions

Q: Should I mock the database in integration tests?
A: No — the point is verifying real interactions, including the database. Use a separate test database. Mocking it turns it into a unit test and misses the integration bugs you’re trying to catch.

Q: How do I keep integration tests fast?
A: Use transaction rollback between tests instead of truncating/recreating tables. Run tests in parallel where isolation allows. Use a containerized database and keep it small.

Q: How many integration tests do I need?
A: Cover each endpoint’s critical paths — success, validation errors, auth failures, and not-found cases. Unit tests handle logic details; integration tests cover the main behaviors of each endpoint.

Q: Integration tests or E2E tests?
A: Integration tests verify the API layer (routes + database). E2E tests verify full user flows through the UI. Both have value — have more integration tests than E2E since they’re faster and more focused.

Q: How do I test external API calls?
A: Mock those specific calls (you don’t want tests hitting third-party services) while keeping your database real. Mock only true external dependencies, not your own database.

Conclusion

Integration tests verify your API works end to end — routes, database, and logic together — catching wiring bugs that unit tests miss. Use Supertest with Jest (Node.js) or pytest with TestClient (Python), point them at a real test database (reset between tests via transaction rollback for speed), and cover success cases, validation errors, auth, and not-found paths. Run them in CI with a service database. Combined with unit tests for logic, integration tests give you confidence your API actually works as clients expect.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা