Module 5: Integration Testing Real Components Together

5. Solitary versus sociable tests

Description

Lesson 4 gave you the rule of what to double and what to keep real; now we're going to put a name on the degrees of that decision. Because "how many of the unit's neighbors you leave real" isn't a yes-or-no switch: it's a dial, and the ends of that dial have precise names the industry uses to talk without ambiguity. A solitary test leaves real only the unit under test and doubles all its neighbors; it is, strictly speaking, the isolated unit test as always. A sociable test leaves real the unit and one or more of its collaborators, to test how they behave together. Between the two there's a spectrum, and knowing which point you're standing on is what lets you choose the right level of reality for what you want to test.

These names aren't academic decoration. When someone says "I wrote a book test", the word doesn't tell you whether they doubled everything (solitary, a unit test) or left the repository real (sociable, an integration). And that difference changes everything: what the green guarantees, how long it takes, what bugs it catches, what costs it inherits. A precise vocabulary dissolves whole arguments: instead of fighting over whether "that's a unit test or an integration one", you say "it's a sociable test with the real repository and the rest doubled", and everyone knows exactly what you tested. This lesson gives you that vocabulary, applies it to Reservo with the two extremes side by side, and shows you why the most useful point is almost never a pure extreme, but the partial sociable: real only the neighbor whose joint matters to you, doubled the rest.

Connection to the module: this lesson names what lessons 2 to 4 built. Lesson 2 defined the integration by its seam; lesson 4 decided what to leave real around it; this one puts the labels —solitary, sociable— on that decision, so you can communicate and reason about it. It's also the bridge to the reward: lesson 6 will compare a solitary bookcancelget (all doubled, with the fake) against the same sociable flow (real repository), and you'll see that the solitary one passes while the sociable one catches the bug. Without this lesson's names, that comparison would be confusing; with them, it's the clean demonstration of why integration —the sociable test with the real piece— sees what the isolated one doesn't.

Analogy: the theater rehearsal

Think of how a theater company rehearses a play. At first, each actor rehearses alone: they go over their lines in front of the mirror, an assistant gives them their cues by reading them from a script without acting, without emotion, just so they know when to speak. The actor practices their part in isolation, with stand-ins who barely mark the entrances. That's a solitary rehearsal: the real actor, all their partners replaced by someone who only reads the cue. It works for memorizing lines, but it doesn't test the play —nobody knows yet whether the scene works when two real actors respond to each other—.

Later comes the rehearsal with the cast: the real actor stands in front of another real actor, and for the first time the scene breathes. They discover things the solitary rehearsal never showed: that a line comes too fast, that two characters block each other when they move, that one's gesture changes the other's timing. That's a sociable rehearsal: the real actors interacting. And they rarely rehearse the whole play with the fifty people, the costumes, the lights, and the audience all at once —that's the premiere, expensive and without a net—; they rehearse scene by scene, two or three real actors at a time, the rest marked. That partially sociable rehearsal —the actors of the scene that matters, real; the rest, stand-ins— is where the play is polished. In Reservo, the solitary actor is book with everything doubled; the rehearsal with the cast is book with the real repository; and the partially sociable rehearsal —real repository, payment and email marked with doubles— is the workhorse of your integrations.

The two ends of the dial

Let's define the terms precisely, about the unit under test (in Reservo, BookingService):

Solitary test. The unit is real; all its collaborators are doubled. Zero real neighbors. It's exactly the isolated unit test: book with FakeBookingRepository, StubPaymentGateway, SpyEmailSender, and FixedClock. It verifies the unit's orchestration logic —that it calls its collaborators in the proper order, with the correct data— assuming they all behave like the doubles. Fast, precise, deterministic, without any real joint under test.

Sociable test. The unit is real and leaves one or more of its collaborators real. At least one real neighbor. book with the real SqliteBookingRepository (even if the payment, the email, and the clock stay doubled) is sociable: the unit "socializes" with a real neighbor, the repository, and tests how they behave together. It is, by lesson 2's definition, an integration test —there are real pieces on both sides of a seam—.

The word "sociable" is literal: it measures whether the unit gets along with its real neighbors. And here's the nuance that matters most in practice: you almost never want a fully sociable test —leaving the payment and the email real means charging cards and sending emails, exactly what lesson 4's rule says to double—. What you want, almost always, is a partially sociable test: real the neighbor whose joint interests you (the repository), doubled the others so you don't inherit their cost and fragility. That mix —one real neighbor, the rest doubled— is exactly the integration you built in lesson 4, and now it has a name.

Worked example: solitary and sociable, side by side

Let's see the two extremes in code, on the same book. The first test is solitary: real BookingService, all its neighbors doubled (the repository is a FakeBookingRepository). The second is sociable: the same book, but with the real SqliteBookingRepository as a neighbor. The only difference between the two is which piece is plugged into the repository seam —an experiment with a single variable—.

# tests/test_solitary_vs_sociable.py — solitary (all doubled) vs sociable (real repo)
import sqlite3
from datetime import datetime

from reservo.calendar import Calendar
from reservo.doubles import (FakeBookingRepository, 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)


def build(repo):
    return BookingService(Calendar(), FixedClock(CLOCK),
                          StubPaymentGateway(ok=True), SpyEmailSender(), repo)


# SOLITARY: the real unit, ALL its neighbors doubled (including the repo)
def test_solitary_book_all_neighbors_doubled():
    repo = FakeBookingRepository()               # doubled neighbor
    booking = build(repo).book(FOCUS, ANA, START, END)
    assert repo.get(booking.id).status == "confirmed"


# SOCIABLE: the real unit converses with a REAL neighbor (the SQLite repo)
def test_sociable_book_with_a_real_neighbor():
    repo = SqliteBookingRepository(sqlite3.connect(":memory:"))   # real neighbor
    booking = build(repo).book(FOCUS, ANA, START, END)
    assert repo.get(booking.id).status == "confirmed"

The two tests share the build(repo) helper, which builds BookingService with the payment, email, and clock doubled; the only thing that changes is the repo they receive. The solitary one gets the fake (zero real neighbors); the sociable one gets the real SQLite (one real neighbor). Let's run them.

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

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

tests/test_solitary_vs_sociable.py::test_solitary_book_all_neighbors_doubled PASSED [ 50%]
tests/test_solitary_vs_sociable.py::test_sociable_book_with_a_real_neighbor PASSED [100%]

============================== 2 passed in 0.02s ===============================

Two greens, two different kinds of test. The solitary one is a unit test: it verifies book's logic with all the neighbors doubled; its green asserts "the orchestration is correct assuming the doubles". The sociable one is an integration: it verifies that book and the real SqliteBookingRepository collaborate; its green asserts "these two real pieces understand each other at this seam". Notice that both pass for this assertion —the confirmed status, which is text and crosses the seam without changing shape—. The difference between solitary and sociable isn't always visible in the result when everything goes well; it's visible when something at the real seam diverges. There the solitary one, blind to the real thing, stays green, and the sociable one catches it. That's exactly lesson 6's scene, with the datetime, in the complete flow.

And notice the design decision of both: the payment, the email, and the clock are doubled in both tests. Neither is fully sociable —neither charges cards or sends emails—. The "sociable" here is partial: real only the repository, the joint that matters to us. That's the workhorse, the point of the dial where almost all practical integration lives.

Why the partial sociable is the sweet spot

Let's go through the dial to understand why the partial extreme wins almost always.

Fully solitary (all doubled) is fast and precise, but it tests no real joint: it's the unit test, indispensable at the base of the pyramid, but not an integration. Its limit is the one that motivates the whole guide: it can be green over a broken system, because the doubles can lie.

Fully sociable (all the neighbors real: payment, email, clock, and database, all real) tests all the joints at once, but inherits all the costs: it charges money, sends emails, depends on external systems, is slow, is flaky. It's an end-to-end, expensive and out of this guide's bounds. Besides, when it fails, you don't know which of the many real joints broke.

Partially sociable (real only the neighbor whose joint matters, doubled the rest) is the sweet spot: it tests one real joint —the one you're worried about— without inheriting the cost of the others. It's fast (only one real resource, in-process), deterministic (the clock doubled), without effects (the payment and email doubled), and when it fails, the suspect is the only real joint you left. It's lesson 4's integration, now with its technical name: a sociable test, but partial.

The practical recommendation is clear: when you integrate, be partially sociable. Leave real the neighbor of the seam under test, double the others. You reserve the fully sociable (the end-to-end) for the very few cases where the question is, precisely, about the whole chain with everything real —and that, in this guide, is the sister guide's territory—.

Common mistakes

Saying "unit test" or "integration test" without saying how many real neighbors. What happens: two people argue whether "the book test" is a unit or an integration one and don't understand each other, because one wrote it solitary and the other sociable. Why it happens: the coarse terms hide the variable that matters (how many real neighbors). How to detect it: if a conversation about "the book test" goes in circles, each person is probably imagining a different point on the dial. How to fix it: ground it with the precise names —"it's solitary, all doubled" or "it's sociable with the real repository, the rest doubled"—. Precision dissolves the argument, because almost always the two people are right about different tests.

Making the test fully sociable "to test more". What happens: someone leaves the payment and the email real in addition to the repository, believing they cover more that way. Why it happens: more real neighbors feels like more coverage. How to detect it: if your sociable test charges cards, sends emails, or is slow, you went to the expensive end of the dial. How to fix it: be partially sociable —real only the neighbor of the joint under test—. The other joints are covered by other tests (unit, contract), cheaper; you don't need to leave them real in this one. Testing more joints in a single test isn't a virtue: it's accumulating cost and failure ambiguity.

Believing the sociable replaces the solitary. What happens: someone, convinced that integration is superior, deletes the solitary unit tests and leaves only the sociable ones. Why it happens: if the sociable one catches bugs the solitary one doesn't see, the sociable seems always better. How to detect it: if your suite became slow and all your tests touch a real resource, you inverted the pyramid. How to fix it: the solitary and the sociable are complementary. The solitary (unit) gives you speed and precision over the logic —the wide base—; the sociable (integration) gives you confidence in the real joints —the middle band—. It's not one or the other: it's many solitary and a few sociable, each in its place.

Exercises

Exercise 1 — Classify each test. For each one, say whether it's solitary, partially sociable, or fully sociable, and why: (a) book with fake, payment stub, email spy, and fixed clock; (b) book with real SqliteBookingRepository, payment stub, email spy, and fixed clock; (c) book with real SqliteBookingRepository, real PaymentGateway, real SmtpEmailSender, and real datetime.now(); (d) cancel with real SqliteBookingRepository and the rest doubled.

See solution
  • (a) Solitary. All the neighbors doubled (fake, stub, spy, fixed clock); zero real neighbors. It's the isolated unit test of book.
  • (b) Partially sociable. One real neighbor (the SQLite repository), the rest doubled. It's the module's canonical integration: real the repository seam, doubled the expensive. The workhorse.
  • (c) Fully sociable. All the neighbors real: real database, payment, email, and clock. It's an end-to-end —it charges cards, sends emails, is flaky because of the real clock, and depends on external services—. Expensive, fragile, and out of this guide's bounds.
  • (d) Partially sociable. One real neighbor (the repository), the rest doubled, this time on the cancel flow. Same as (b) but for another operation. Still the sweet spot.

The pattern: count the real neighbors. Zero → solitary. One or a few (the one of the joint that matters) → partially sociable. All → fully sociable. And the sweet spot, almost always, is "a few": the real neighbor you test, doubled the rest.

Exercise 2 — The same green, two claims. The solitary (a) and the partially sociable (b) from the previous exercise both pass green for assert repo.get(booking.id).status == "confirmed". Write the exact claim each green guarantees, and explain in which concrete scenario one would stay green while the other went red.

See solution
  • Solitary green (a): it guarantees "book's logic saves a booking with confirmed status, assuming the repository behaves like the FakeBookingRepository". It's a conditional claim about the orchestration, with the repository's behavior taken as good (the fake).
  • Sociable green (b): it guarantees "BookingService and the real SqliteBookingRepository collaborate: a booking created by book is written to the real database and read back with confirmed status". There's no more assumption about the repository; the real one was exercised.

The scenario where they diverge: imagine the real SqliteBookingRepository had a bug in save —for example, that it saved the status in a column with a CHECK constraint that rejects 'confirmed', or that it serialized the status wrong—. The solitary (a) would stay green, because the fake doesn't have that bug: it stores the object as-is. The sociable (b) would go red, because the real seam would trip over the problem when writing or reading. There you see the difference: when the real seam diverges from the fake, the solitary is blind and the sociable catches it. It's, in miniature, lesson 6's scene.

Exercise 3 — Design the test ladder for cancel. You want to cover the cancel flow with the right combination of solitary and sociable tests. Describe what tests you'd set up, from solitary to sociable, and what each gives you.

See solution

A reasonable ladder, from the wide base upward:

  1. Solitary (unit) tests of the refund logic. cancel with all the neighbors doubled (fake, stub, spy, fixed clock), parametrized by the three anchors: 72 h → 6000, 36 h → 3000, 12 h → 0. Fast, precise, many. They verify that cancel's logic computes the correct refund assuming the doubles. It's most of your confidence about the logic.
  2. A repository contract (modules 3-4). The parametrized battery that runs the repository's clauses against the fake and against SQLite. It guarantees that the fake doesn't lie about what the contract covers. It's neither solitary nor sociable of cancel: it's the certification of the neighboring piece.
  3. One or two partially sociable of cancel. cancel with the real SqliteBookingRepository and the rest doubled, to verify that the complete flow —read the booking from the real one, recompute, save cancelled, re-read— works with the real database. Few, because they're more expensive. This is where, in lesson 6, the datetime's TypeError will come out if the repository doesn't convert back.

The shape is the pyramid: many solitary (logic), one contract (the certified neighbor), a few sociable (the real joint). Each rung covers a different class of failure —the logic, the double's divergence, the real collaboration— and none replaces the others. That's the mature suite for cancel.

Summary and next step

In this lesson you put a name on the degrees of lesson 4's decision. A solitary test leaves real only the unit and doubles all its neighbors —the isolated unit test—; a sociable test leaves one or more neighbors real —an integration—. With the theater rehearsal you understood the difference between the actor practicing alone with stand-ins (solitary) and the scene with the real cast (sociable), and why the scene-by-scene rehearsal, with the actors that matter real and the rest marked, is where the play is polished. You saw book solitary and book sociable side by side, both green, and understood that the difference is revealed when the real seam diverges. And you located the sweet spot: the partial sociable —real the neighbor whose joint matters, doubled the rest—, the workhorse of practical integration.

Before moving on you should be able to: classify any test as solitary, partially sociable, or fully sociable by counting its real neighbors; translate the green of a solitary (conditional) and that of a sociable (about the real thing) and say in which scenario they diverge; and design a test ladder that combines solitary, contract, and sociable without one replacing the others.

We reach the module's climax. Up to here, all the sociable tests we ran passed green —we kept the assertions within what crosses the seam without trouble—. In lesson 6 we're going to take the complete flow bookcancelget against the real repository and see how integration catches what neither the unit nor the isolated contract sees: the datetime that came back as str and that cancel can't subtract, a TypeError in the live flow. The solitary passes; the sociable blows up; and there, at last, you collect everything integration gives you back.

Resources

  • Martin Fowler — UnitTest (solitary vs sociable) — the source that coins "solitary" and "sociable" and discusses the two schools that prefer each; the exact frame of this lesson.
  • Martin Fowler — IntegrationTest — the complement on why a sociable test with a real piece is, by definition, an integration, and how it relates to the scope (narrow/broad).
  • pytest documentation — Parametrizing tests — the reference for exercise 3's test ladder, where the solitary tests of cancel's three anchors are written as a single parametrized test.
  • test-doubles-and-test-data-guide — the sister guide on the doubles a solitary test uses for all its neighbors and a partial sociable for all but one; useful to remember which double corresponds to each collaborator when you build any point on the dial.