Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Unit tests verify that each piece works in isolation. Integration tests verify that pieces work together: the database layer actually writes what the service layer tells it to, the HTTP handler actually calls the right service method with the right parameters, the message queue consumer actually processes messages in the right order. Integration tests are slower than unit tests (they need real infrastructure — even if containerized) but they catch an entire class of bugs that unit tests miss: contract mismatches, serialization errors, database constraint violations, and auth middleware bugs. They're the middle layer of the test pyramid: fewer than unit tests, more than E2E.
Integration tests verify that two components work together correctly: the FastAPI handler dispatches to the right service method, the service writes the right data to the database, and the database enforces the constraints the application assumes. Running these tests against a real database — even a lightweight SQLite in-memory instance — catches contract mismatches, serialization bugs, and constraint violations that unit tests with mocked repositories can never expose. This is why integration tests occupy the middle tier of the pyramid: you need fewer of them than unit tests, but they cover a class of bugs that only exist at integration boundaries.
# pip install fastapi httpx pytest pytest-asyncio sqlalchemy
# Integration test: FastAPI endpoint + SQLite in-memory database (no mocks)
import pytest, asyncio
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import DeclarativeBase, Session
# ── Minimal app ──────────────────────────────────────────────────────────────
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
def create_app(db_engine):
app = FastAPI()
@app.post("/users", status_code=201)
def create_user(payload: dict):
with Session(db_engine) as session:
user = User(email=payload["email"], name=payload["name"])
session.add(user)
session.commit()
session.refresh(user)
return {"id": user.id, "email": user.email, "name": user.name}
@app.get("/users/{user_id}")
def get_user(user_id: int):
with Session(db_engine) as session:
user = session.get(User, user_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404)
return {"id": user.id, "email": user.email, "name": user.name}
return app
# ── Test fixtures ────────────────────────────────────────────────────────────
@pytest.fixture
def client():
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
app = create_app(engine)
with TestClient(app) as c:
yield c
# ── Integration tests — real HTTP + real DB, no mocks ───────────────────────
def test_create_user_persists_to_database(client):
resp = client.post("/users", json={"email": "alice@test.com", "name": "Alice"})
assert resp.status_code == 201
created_id = resp.json()["id"]
# Verify it's actually in the DB by fetching it
get_resp = client.get(f"/users/{created_id}")
assert get_resp.status_code == 200
assert get_resp.json()["email"] == "alice@test.com"
def test_get_nonexistent_user_returns_404(client):
resp = client.get("/users/9999")
assert resp.status_code == 404
def test_duplicate_email_rejected(client):
client.post("/users", json={"email": "dup@test.com", "name": "User1"})
resp = client.post("/users", json={"email": "dup@test.com", "name": "User2"})
# SQLite unique constraint violation → 500 (in a real app: 409 Conflict)
assert resp.status_code in (409, 500)python3 main.pyGET /users (add this endpoint) and asserts the list has 3 items. This tests the list endpoint — a common integration test for REST APIs.POST /users with no email. Assert the response is 400 with an error message. Then verify that no user was created by checking the DB count. This verifies both the validation and the side effect.pytest -v --durations=10. What is the ratio? This is the data behind why you have more unit tests than integration tests.Use these three in order. Each builds on the one before.
In one paragraph, explain what an integration test tests that a unit test doesn't. Give a concrete example where all unit tests pass but an integration test fails.
Walk me through why using an in-memory SQLite database for integration tests is better than mocking the database layer. What does the integration test verify that a mock can't?
I'm designing an integration test strategy for a microservices system: 5 services, each with its own database, communicating via REST and a message queue. Walk me through the testing boundaries: which interactions to test with integration tests, which need contract tests, which to cover with E2E tests, and how to keep the integration tests fast enough to run on every PR.