Integration Testing
Integration Testing
Section titled “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.
When Integration Tests Add Value
Section titled “When Integration Tests Add Value”- 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)”npm install -D supertest @types/supertestimport 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); });});Testing with a Real Database
Section titled “Testing with a Real Database”Use a test database (not the dev or prod database):
export default { globalSetup: './tests/setup.ts', globalTeardown: './tests/teardown.ts',};
// tests/setup.tsimport { 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:
npm install -D testcontainersimport { 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 testsusing 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"); }}Testing with Python (pytest + FastAPI)
Section titled “Testing with Python (pytest + FastAPI)”from fastapi.testclient import TestClientfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmaker
SQLALCHEMY_TEST_URL = "sqlite:///:memory:"engine = create_engine(SQLALCHEMY_TEST_URL)
@pytest.fixturedef 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"Best Practices
Section titled “Best Practices”- 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