█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Software Engineering/Testing/Integration Testing
45 minIntermediate

Integration Testing

After this lesson, you will be able to: Test multiple components together, including real database interactions and API routes, without falling into the over-mocking trap.

Unit tests prove each piece works alone. Integration tests prove the pieces work together, which is where a huge share of real bugs hide: the API route that calls the database, the serializer that mangles a date, the transaction that does not roll back. This lesson covers testing across a boundary with a real (test) database.

Prerequisites:Unit Testing in Python

What integration tests catch that unit tests cannot

Unit tests mock the database, so they never catch a wrong SQL query, a missing migration, a serialization bug, or a broken transaction. Integration tests run the real code path against a real (but disposable) dependency. The cost is speed: they run in seconds, not milliseconds, so you write fewer of them and target the boundaries that matter most: API route to database, service to external dependency, queue producer to consumer.

Integration test against a real test database

Use a dedicated test database (a Docker Postgres or a fast local SQLite). Reset state between tests. Never touch production.

python
// orders.integration.test.ts
import { beforeEach, afterAll, describe, it, expect } from 'vitest';
import { db } from './db';
import { createOrder, listOrdersForUser } from './orders';
beforeEach(async () => {
// Clean slate so tests don't depend on each other.
await db.query('TRUNCATE orders RESTART IDENTITY CASCADE');
});
afterAll(async () => { await db.end(); });
describe('orders (integration)', () => {
it('persists and reads back an order', async () => {
await createOrder({ userId: 'u1', total: 42.0 });
const list = await listOrdersForUser('u1');
expect(list).toHaveLength(1);
expect(list[0].total).toBe(42.0);
});
it('isolates orders by user', async () => {
await createOrder({ userId: 'u1', total: 10 });
await createOrder({ userId: 'u2', total: 20 });
expect(await listOrdersForUser('u1')).toHaveLength(1);
});
});

Testing an API route end to end (without a browser)

Drive the HTTP layer directly with a client like Supertest (Node) or httpx/TestClient (Python). This tests routing, validation, handler, and DB together.

python
// api.integration.test.ts
import request from 'supertest';
import { app } from './app';
it('rejects an invalid payload with 400', async () => {
const res = await request(app).post('/api/orders').send({ total: -5 });
expect(res.status).toBe(400);
});
it('creates an order and returns 201', async () => {
const res = await request(app)
.post('/api/orders')
.send({ userId: 'u1', total: 42 });
expect(res.status).toBe(201);
expect(res.body).toMatchObject({ userId: 'u1', total: 42 });
});

💡 Testcontainers: a real dependency, disposable

For databases, queues, or caches, Testcontainers spins up a throwaway Docker container per test run, so your integration tests hit a real Postgres/Redis that is created and destroyed automatically. It removes the 'works against SQLite, breaks against real Postgres' class of bug while keeping tests self-contained.

Quick Check

Why mock the database in unit tests but use a real one in integration tests?

Pick the best answer.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Unit Testing in Python
Back to Testing
End-to-End Testing with Playwright→