Module 1: From Units To Integration

6. Types of integration

Description

Until now we've used "integration test" as if it were a single thing: two real pieces together crossing a seam. In practice there are several kinds, and confusing them is a source of endless arguments —two people say "integration test" and mean different things—. This lesson gives you the axes to name precisely what you're testing: how many real pieces you put in, how far the scope reaches, and in what order you assemble. It's not taxonomy for its own sake: each axis corresponds to a concrete decision you'll make when writing a test, and having the name lets you make it deliberately.

We'll look at three axes. The first, solitary vs sociable, distinguishes whether you leave real only the unit under test (doubling its neighbors) or whether you leave the neighbors real too. The second, narrow vs broad, distinguishes whether the test touches a single seam (the repository against its database) or a journey that crosses several (a complete book with a real repository). The third, incremental vs big-bang, distinguishes whether you integrate the pieces bit by bit, joint by joint, or all at once. Each axis applied to Reservo, so that the next time you write a test you know exactly which point of each axis you're standing on and what you leave out.

Connection to the module: this lesson sharpens the vocabulary lesson 2 left coarse. Lesson 2 gave you "unit vs integration"; here you see that within "integration" there are degrees, and that the choice between them is what populates the middle of the pyramid (lesson 3). The types you name here are the ones lesson 7 will weigh by cost, and the ones you'll write in depth from module 5 on. Knowing there's a spectrum —and not a single "integration test"— is what lets you choose the right amount of real you need: neither so little that you don't test the joint, nor so much that you inherit the slowness and fragility of an e2e.

Analogy: testing a restaurant kitchen

Think of a restaurant that's launching a new kitchen and wants to make sure it works before opening. There are many ways to "test the kitchen together", and each answers something different. It can test one station with its real neighbors: the grill cook working with the real assistant who hands them the plates and the real pass that picks them up —that's sociable, the grill surrounded by its real collaborators—; or test the grill alone, with a stand-in assistant who only pretends to hand it the plates —that's solitary, the station isolated—. It can test a single connection: does the waiter call out the order and the cook receive it correctly? —that's narrow, one joint—; or test a complete order end to end: the order comes in, gets cooked, gets plated, goes out to the dining room —that's broad, several joints in a chain—. And it can assemble the kitchen bit by bit —first grill+pass, then the fryer is added, then dessert—, catching the problem as soon as a new piece comes in —that's incremental—; or turn on everything at once on opening night and see what breaks —that's big-bang, and when something fails, good luck figuring out which of the ten stations it was—.

Reservo is that kitchen. The "grill with real neighbors" is BookingService with the real repository; the "grill alone" is BookingService with everything doubled; the "single connection" is the repository against its database; the "complete order" is a book that crosses payment, persistence, and email. And none of these tests is the integration test: they're different questions about the joints, each with its own name. A good team, like a good chef, knows which one it needs at each moment.

Axis 1: solitary vs sociable

This axis is about how many of the unit's neighbors you leave real.

  • Solitary test: the unit under test is real; all its collaborators are doubled. It's, strictly speaking, the isolated unit test as always —book with fake, stub, and spy—. We mention it here because it's the extreme of the axis: zero real neighbors.
  • Sociable test: the unit is real and leaves one or more of its collaborators real, to test how they behave together. BookingService with the real SqliteBookingRepository (even if the payment and email stay doubled) is sociable: the unit "socializes" with a real neighbor.

The word "sociable" is literal: it measures whether the unit gets along with its real neighbors. In Reservo you almost never want a fully sociable test —leaving the payment and email real means charging cards and sending emails—; the usual thing is a partially sociable test: real exactly the neighbor whose joint matters to you (the repository), doubled the rest so you don't inherit their cost and fragility. That mix —one real piece, the rest doubled— is the workhorse of practical integration, and you'll see it over and over.

Axis 2: narrow vs broad

This axis is about how many seams the test crosses.

  • Narrow integration: the test touches a single seam, with the minimum real pieces to exercise it. SqliteBookingRepository.get of a missing id raising against the real database is as narrow as possible: one piece (the repository), one seam (repository ↔ database), one question (does it raise when it doesn't exist?). Fast, precise, easy to locate when it fails.
  • Broad integration: the test runs across several seams in a chain. A complete book with a real repository crosses the payment seam (doubled), the persistence one (real), and the email one (doubled) in a single flow. It tests more joints at once, in exchange for more setup and a more ambiguous failure (which seam in the chain broke?).

The narrow-broad axis is a trade-off dial. The narrower, the more it resembles a unit test in its virtues (fast, precise) but testing a real joint. The broader, the more it resembles an e2e (realistic, but slow and ambiguous). Narrow integration is the one that yields most for its cost, and that's why, when you can, prefer to verify a seam narrowly before wrapping it in a broad flow: if the datetime diverges, a narrow round-trip test of the repository tells you faster and more clearly than a complete book.

Axis 3: incremental vs big-bang

This axis is about the order in which you assemble the real pieces.

  • Incremental integration: you integrate bit by bit, one joint at a time. First you verify the repository against its database (narrow); when that joint is solid, you add BookingService on top (sociable); only then, if needed, a broader flow. Each time you add a real piece, if something breaks, you know it was the one you just added. The failure comes with its culprit pointed out.
  • Big-bang integration: you connect all the real pieces at once and test the whole. It's tempting —"let's test it all together at once"—, and it's where the worst debugging headache is born: when the whole fails, the bug could be in any of the pieces or in any of the joints, and you have no order to tell you where to start.

The practical recommendation is almost always incremental, and for the same reason as the pyramid: early, localized failure is worth gold. Build your Reservo integration tests from the bottom up —the repository seam first, the sociable service later— so that each red points to the just-integrated piece, not to a cloud of suspects.

Worked example: narrow and sociable, side by side

Let's see two of these types in code, on Reservo. The first is a narrow integration: a single real piece (the repository) against its real resource (the database), verifying a single thing —that get of a missing id raises—. The second is sociable: real BookingService conversing with the real repository, with the payment and email doubled so as not to inherit their cost.

# tests/test_integration_types.py — narrow vs sociable
import sqlite3
from datetime import datetime

import pytest

from reservo.calendar import Calendar
from reservo.doubles import FixedClock, SpyEmailSender, StubPaymentGateway
from reservo.models import Member, Room
from reservo.services import BookingService
from reservo.sqlite_repo import SqliteBookingRepository

FOCUS = Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
ANA = Member(id="m-ana", name="Ana", tier="pro")
START = datetime(2026, 3, 10, 9)
END = datetime(2026, 3, 10, 12)
CLOCK = datetime(2026, 3, 1, 9)


# NARROW — a single real piece against its real resource
def test_narrow_get_missing_id_raises_against_real_db():
    repo = SqliteBookingRepository(sqlite3.connect(":memory:"))
    with pytest.raises(KeyError):
        repo.get("does-not-exist")


# SOCIABLE — real BookingService + real repo; payment and email doubled
def test_sociable_book_persists_through_the_service():
    repo = SqliteBookingRepository(sqlite3.connect(":memory:"))
    service = BookingService(Calendar(), FixedClock(CLOCK),
                             StubPaymentGateway(ok=True), SpyEmailSender(), repo)

    booking = service.book(FOCUS, ANA, START, END)

    assert repo.get(booking.id).status == "confirmed"

What to expect. On my machine (Python 3.14.0, pytest 9.1.1):

python3 -m pytest tests/test_integration_types.py -v
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 2 items

tests/test_integration_types.py::test_narrow_get_missing_id_raises_against_real_db PASSED [ 50%]
tests/test_integration_types.py::test_sociable_book_persists_through_the_service PASSED [100%]

============================== 2 passed in 0.01s ===============================

Two greens, two different types of integration. Compare their shapes. The narrow one is tiny: it creates the real repository, asks for a nonexistent id, verifies that it raises. One piece, one seam, one question —if it fails, you know exactly what—. The sociable one builds BookingService with four collaborators, three of them doubled and one real (the repository), runs a complete book and then verifies that the booking ended up confirmed in the real database. It crosses more ground —the orchestration logic and the real persistence— in exchange for a larger setup. Notice the sociable one's design decision: it leaves real only the repository (the joint that matters) and doubles the payment and email (which would cost real money and emails). That's the partially sociable test we were talking about: real where the risk lives, doubled where the cost hurts.

Another useful cut: component integration vs subsystem integration

Besides the three axes, you'll find two scope labels worth recognizing:

  • Component integration: verifies that two concrete components fit together —BookingService and SqliteBookingRepository—. It's what we've been doing all lesson.
  • Subsystem integration: verifies that a larger group of components, a complete "subsystem", works assembled —for example, Reservo's whole booking flow with its real persistence, but still without the web interface or the real payment gateway—.

This guide's border lives right here: we reach component integration and in-process subsystem integration (with SQLite and, in module 6, a minimal stdlib http.server). The complete subsystem with a real web framework —the whole app responding to real HTTP requests— is already higher-level integration and belongs to testing-backend-applications-guide. Naming the levels helps you see where our terrain ends and the sister guide's begins.

Common mistakes

Saying "integration test" without specifying which. What happens: two people argue about "the integration tests" and don't understand each other because one is thinking of the narrow round-trip of the repository and the other of a broad subsystem flow. Why it happens: the term, on its own, is ambiguous. How to detect it: if a conversation about integration goes in circles without agreement, each person is probably at a different point of the axes. How to fix it: ground the term with the axes —"a sociable and narrow test of the repository", "a broad test of book"—. Precision dissolves the disagreement, because almost always the two people are right about different things.

Always choosing the broadest "to test more". What happens: someone wraps every check in a complete book with a real repository, believing it covers more. Why it happens: the broad feels more realistic and therefore more valuable. How to detect it: if you verify the datetime divergence through a whole book when a narrow round-trip of the repository was enough, you're paying extra setup and ambiguity. How to fix it: prefer the narrowest test that can answer your question. The broad is reserved for when the question is, precisely, about the whole chain; for a single joint, the narrow wins in speed and clarity of failure.

Integrating big-bang and debugging blind. What happens: someone connects all the real pieces at once, the whole fails, and spends hours not knowing which piece to blame. Why it happens: "test it all together" seems like the shortcut. How to detect it: if your integration failures never point to a concrete piece and always demand an investigation, you're integrating big-bang. How to fix it: assemble incrementally —first the repository seam, then the sociable service on top—, so that each new piece you add is the obvious suspect when something breaks. The assembly order is a debugging tool, not a detail.

Exercises

Exercise 1 — Place each test on the three axes. For each one, say where it falls on solitary/sociable, narrow/broad, and incremental/big-bang (where applicable): (a) SqliteBookingRepository.save followed by find_by_room verifying that the row landed; (b) book with a real repository, doubled payment and email, verifying persistence and email; (c) book with all four collaborators real (real payment and email included).

See solution
  • (a) save + find_by_room of the real repo — solitary as far as neighbors go (there's no other unit, just the repository), narrow (one seam: repository ↔ database), and it's the base piece of an incremental integration. It's the most economical and precise integration test: one piece, one joint.
  • (b) book with real repo, doubled payment and email — partially sociable (one real unit, BookingService, with one real neighbor, the repository, and two doubled), broad (crosses payment, persistence, and email in a flow), and the second step of an incremental integration (you add BookingService on top of the repository seam already verified in (a)). The practical workhorse.
  • (c) book with all four real — fully sociable and very broad, tending toward big-bang. It's an e2e in disguise: it charges cards and sends real emails. Outside this guide —and in general, something you almost never want, because of cost, fragility, and ambiguity of failure—.

The pattern: as you leave more real neighbors (more sociable) and cross more seams (broader), you gain realism but lose speed, precision, and failure control. Reservo's sweet spot is (b) reduced to the essentials —or directly (a)— depending on which joint you want to verify.

Exercise 2 — Design Reservo's incremental integration. You want to cover the repository seam and its use from book with integration. Write the incremental order of tests you'd set up, and say what each possible red tells you.

See solution

The incremental order, from the smallest joint upward:

  1. SqliteBookingRepository against its database, narrow. Tests: save+get preserves the fields; get of a missing id raises; save twice of the same id updates (doesn't duplicate); find_by_room filters correctly. If something here goes red, the bug is in the repository or its SQL —a single piece, easy to locate—.
  2. BookingService + real repository, sociable (payment and email doubled). Test: a complete book persists a confirmed, readable booking in the real database. Since layer (1) is already green, if (2) goes red the suspect is the new joint: how BookingService uses the repository (the order of the calls, the data it passes it), not the repository itself.

The gain of this order: each red arrives with its culprit bounded. A red in (1) is "the repository"; a red in (2), with (1) green, is "how the service uses the repository". If you'd integrated big-bang —book with real repo from the start, without layer (1)—, a red would leave you doubting between the repository's SQL and the service's logic, and you'd have to untangle it by hand.

Exercise 3 — Narrow or broad for this bug. You suspect that SqliteBookingRepository saves the status wrong (the one from exercise 2 of lesson 2). You have two options to catch it: (a) a broad book with a real repository that verifies repo.get(id).status == "confirmed"; (b) a narrow test that does save of a booking with status="confirmed" directly to the repository and then get and verifies the status. Which do you prefer and why?

See solution

I prefer (b), the narrow one, for two reasons.

First, precision on failure. The bug you suspect lives in the SQL of SqliteBookingRepository.save/get, a single seam. The narrow test exercises exactly that seam and nothing else: if it fails, the culprit is unambiguous. The broad test (a) puts all of book's logic in between —validation, price computation, the orchestration—, so a red could be due to something else, and you'd have to rule out the service before looking at the repository.

Second, speed and simplicity. The narrow one doesn't need to build BookingService with its four collaborators, nor a Calendar, nor a FixedClock; just the repository and a booking. Less scaffolding, less that can go wrong for reasons unrelated to the bug, and it runs in a blink.

The general rule you're applying: to verify a concrete joint, use the narrowest test that exercises it. The broad is reserved for when the question is about the whole chain (does book persist correctly end to end?), not for catching a bug you already know lives at a single seam. The narrow one is to integration what the unit test is to logic: the precise tool.

Summary and next step

In this lesson you traded "integration test" —a coarse term— for a vocabulary with an edge. You learned three axes: solitary vs sociable (how many real neighbors you leave), narrow vs broad (how many seams you cross), and incremental vs big-bang (in what order you assemble). You saw, with the narrow test of the repository and the sociable one of book, two concrete points of that space, and why the partially sociable test —one real piece, the rest doubled— and narrow, incremental integration are the workhorses: maximum joint value for the minimum of cost and ambiguity. And you located the guide's border: we reach component and in-process subsystem integration; the subsystem with a web framework is the sister guide's.

Before moving on you should be able to: place any integration test on the three axes; explain why the narrow and incremental wins in failure precision; and choose the right amount of "real" for a given question, without falling into the fully sociable big-bang.

Each of these types has a different cost, and until now we've named it without measuring it. Lesson 7 puts numbers on the table: how much slower an integration test really is than a unit test —we'll measure it with the FakeBookingRepository against the on-disk SqliteBookingRepository— and how to decide, with that cost in hand, when integration is worth it and when a contract is enough.

Resources