Skip to content

Integration Testing

Integration tests verify that multiple components work correctly together. Unlike unit tests, they use real implementations — real databases, real HTTP clients — to catch issues at the seams.

  • API endpoints (controller → service → database)
  • Database queries (ORM configuration, SQL correctness, migrations)
  • External service integrations (message queues, file storage)
  • Authentication and authorization flows

Testing an API Endpoint (Node.js + Supertest)

Section titled “Testing an API Endpoint (Node.js + Supertest)”
Terminal window
npm install -D supertest @types/supertest
tests/users.integration.test.ts
import request from 'supertest';
import { app } from '../src/app';
import { db } from '../src/database';
beforeEach(async () => {
await db.users.deleteMany(); // clean state before each test
});
afterAll(async () => {
await db.$disconnect();
});
describe('POST /api/users', () => {
it('creates a user and returns 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
name: 'Alice',
email: 'alice@example.com',
});
});
it('returns 400 when email is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice' })
.expect(400);
expect(response.body.error).toContain('email');
});
it('returns 409 when email already exists', async () => {
await request(app).post('/api/users').send({ name: 'Alice', email: 'alice@example.com' });
await request(app).post('/api/users').send({ name: 'Alice 2', email: 'alice@example.com' })
.expect(409);
});
});
describe('GET /api/users/:id', () => {
it('returns user when found', async () => {
const created = await request(app)
.post('/api/users')
.send({ name: 'Bob', email: 'bob@example.com' });
const response = await request(app)
.get(`/api/users/${created.body.id}`)
.expect(200);
expect(response.body.email).toBe('bob@example.com');
});
it('returns 404 when not found', async () => {
await request(app).get('/api/users/nonexistent-id').expect(404);
});
});

Use a test database (not the dev or prod database):

jest.config.ts
export default {
globalSetup: './tests/setup.ts',
globalTeardown: './tests/teardown.ts',
};
// tests/setup.ts
import { execSync } from 'child_process';
export default async function setup() {
process.env.DATABASE_URL = 'postgresql://localhost:5432/myapp_test';
execSync('npx prisma migrate deploy');
}

Or use Testcontainers to spin up a real database in Docker:

Terminal window
npm install -D testcontainers
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16').start();
process.env.DATABASE_URL = container.getConnectionUri();
await runMigrations();
}, 60_000);
afterAll(async () => {
await container.stop();
});

Testing with a Real Database (C# / ASP.NET Core)

Section titled “Testing with a Real Database (C# / ASP.NET Core)”
// Use WebApplicationFactory for integration tests
using Microsoft.AspNetCore.Mvc.Testing;
public class UsersEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace production DB with SQLite in-memory
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlite("DataSource=:memory:"));
});
}).CreateClient();
}
[Fact]
public async Task Post_CreateUser_Returns201()
{
var response = await _client.PostAsJsonAsync("/api/users",
new { Name = "Alice", Email = "alice@example.com" });
response.StatusCode.Should().Be(HttpStatusCode.Created);
var user = await response.Content.ReadFromJsonAsync<UserResponse>();
user!.Name.Should().Be("Alice");
}
}
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_TEST_URL = "sqlite:///:memory:"
engine = create_engine(SQLALCHEMY_TEST_URL)
@pytest.fixture
def client():
Base.metadata.create_all(bind=engine)
TestingSessionLocal = sessionmaker(bind=engine)
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
Base.metadata.drop_all(bind=engine)
def test_create_user(client):
response = client.post("/users", json={"name": "Alice", "email": "alice@example.com"})
assert response.status_code == 201
assert response.json()["email"] == "alice@example.com"
  • Clean database state between tests — use transactions that rollback, or truncate tables in beforeEach
  • Use a dedicated test database — never run integration tests against dev or prod
  • Test the happy path and key error paths — don’t exhaustively test every input combination (that’s unit test territory)
  • Tag slow integration tests to run separately from fast unit tests