Module 8: Project Contract And Integration For Reservo

8. Project: Reservo's contract + integration

Description

The moment has come to gather everything into a formal delivery. Over seven lessons you built, piece by piece, the three layers of the guarantee; this lesson joins them into a project with a statement, a rubric, and a reference solution, and with it closes the guide. The assignment is concrete: deliver a consumer-driven contract for the BookingRepository, its verification from both sides —against the fake and against the real SQLite, both green—, and an end-to-end integration test of BookingService + real repository, isolated, that exercises bookgetcancel. There are no new concepts: there's synthesis, judgment, and real execution.

What's evaluated, and it's worth saying without hedging, is the method, not the quantity. A project with six well-chosen tests —each with a clear reason to exist— is worth more than one with fifty written by inertia. This lesson's rubric measures whether you know how to decide what to double and what to test real, derive the clauses from the consumer's needs, verify from both sides, isolate the integration, and explain why each piece is where it is. At the end you'll have the whole project running green and the guide closed, with the answer to the question that opened it: which tests are worth it, and why.

Connection to the module and the guide: this lesson is the close of the capstone and of the complete journey. It gathers the deliverables from lessons 2 to 6 and the proof of value from lesson 7, presents them as a formal assignment with its rubric, and offers a complete reference solution —with its real pytest output— against which to contrast your work. And it ends where the guide ends: a review of the eight modules and the map of where to continue in the Testing ecosystem. It's your trial by fire and your graduation.

The assignment

Write, for this guide's Reservo, the following three pieces, run them for real with pytest and the stdlib's sqlite3, and save the output of each.

Deliverable 1 — The BookingRepository's consumer-driven contract. A parametrized test battery with, at minimum, these four clauses, each derived from a real need of BookingService:

  • save-and-read returns the same booking (need: book saves, cancel re-reads);
  • get of an absent id raises (need: cancel must find out if the booking doesn't exist);
  • saving the same id twice updates, doesn't duplicate (need: cancel rewrites the cancelled booking);
  • find_by_room returns only that room's bookings (need: book checks availability, a screen lists the room).

Deliverable 2 — The verification from both sides. A parametrized fixture that runs the battery, without duplicating code, against the FakeBookingRepository and the real SqliteBookingRepository over :memory:. The delivery is the green output of the eight combinations (4 clauses × 2 providers).

Deliverable 3 — The end-to-end integration. A test of BookingService + real SqliteBookingRepository that exercises the complete flow bookgetcancel, with the right mix of real (the repository) and doubled (clock, payment, email), isolated with a fixture that creates and destroys the resource or with a transaction that's reverted. The delivery is the green output of the flow.

Use Booking.price_cents for the money (int cents), identifiers in English, and Reservo's anchors (Focus 3 h pro = 6000 cents; refund 72 h→6000 / 36 h→3000 / 12 h→0).

The rubric: the method is evaluated

Your delivery is judged by five criteria, all about the method. There are no points for volume of tests.

CriterionWhat's soughtSign that it's right
DecisionYou chose what to double and what to keep real, and you know how to justify itReal the repository (the tested seam); doubled clock, payment, and email (non-deterministic, external), with the reason stated
Consumer-driven contractThe clauses come from real needs of BookingService, not from the provider's catalogEach clause can be traced to a line of book or cancel; none mentions internal SQLite details
Both sidesThe contract runs against the fake AND the real one, with a single battery8 passed with ids [fake] and [sqlite], not two twin suites nor a single side
Isolated integrationThe real flow crosses the seam and each run starts cleanThe flow bookgetcancel green, with fixture or rollback; it doesn't contaminate between tests
ExplanationYou can say, for each piece, why it exists and what bug it catchesA short diagnosis: what you doubled and why, what the contract catches, what the integration catches

The way to fail this rubric isn't writing few tests: it's writing many without judgment —doubling the seam you want to test, leaving the contract one-sided, not isolating the integration, or not being able to say why each test exists—. The way to pass it is delivering the three pieces, green, and explaining the method behind each one.

Reference solution

Contrast your work with this complete solution. It's in a collapsible so you first attempt your own; open it when you want to compare.

See the complete reference solution (code and real output)

Deliverables 1 and 2: the contract from both sides

# tests/test_repository_contract.py
import sqlite3
from datetime import datetime

import pytest

from reservo.doubles import FakeBookingRepository
from reservo.models import Booking
from reservo.sqlite_repo import SqliteBookingRepository

START = datetime(2026, 3, 10, 9)
END = datetime(2026, 3, 10, 12)      # Focus 3 h


def a_booking(id="bk-1", room_id="focus", status="confirmed", price_cents=6000):
    return Booking(id=id, room_id=room_id, member_id="m-ana",
                   start=START, end=END, status=status, price_cents=price_cents)


@pytest.fixture(params=["fake", "sqlite"])
def repo(request):
    if request.param == "fake":
        return FakeBookingRepository()
    return SqliteBookingRepository(sqlite3.connect(":memory:"))


def test_save_then_get_returns_the_same_booking(repo):        # clause 1
    booking = a_booking()
    repo.save(booking)
    assert repo.get("bk-1") == booking


def test_get_of_a_missing_id_raises(repo):                    # clause 2
    with pytest.raises(KeyError):
        repo.get("does-not-exist")


def test_saving_the_same_id_twice_updates_not_duplicates(repo):   # clause 3
    repo.save(a_booking(status="confirmed"))
    repo.save(a_booking(status="cancelled"))
    assert repo.get("bk-1").status == "cancelled"
    assert len(repo.find_by_room("focus")) == 1


def test_find_by_room_returns_only_that_rooms_bookings(repo):     # clause 4
    repo.save(a_booking(id="bk-1", room_id="focus"))
    repo.save(a_booking(id="bk-2", room_id="studio"))
    assert [b.id for b in repo.find_by_room("focus")] == ["bk-1"]

Real output:

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

tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[fake] PASSED [ 12%]
tests/test_repository_contract.py::test_save_then_get_returns_the_same_booking[sqlite] PASSED [ 25%]
tests/test_repository_contract.py::test_get_of_a_missing_id_raises[fake] PASSED [ 37%]
tests/test_repository_contract.py::test_get_of_a_missing_id_raises[sqlite] PASSED [ 50%]
tests/test_repository_contract.py::test_saving_the_same_id_twice_updates_not_duplicates[fake] PASSED [ 62%]
tests/test_repository_contract.py::test_saving_the_same_id_twice_updates_not_duplicates[sqlite] PASSED [ 75%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[fake] PASSED [ 87%]
tests/test_repository_contract.py::test_find_by_room_returns_only_that_rooms_bookings[sqlite] PASSED [100%]

============================== 8 passed in 0.03s ==============================

Deliverable 3: the end-to-end integration, isolated

# tests/test_end_to_end_integration.py
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)          # 9 days before -> full refund (6000)


@pytest.fixture
def repo():
    conn = sqlite3.connect(":memory:")          # ephemeral DB, clean per test (isolation)
    yield SqliteBookingRepository(conn)
    conn.close()


def make_service(repo):
    # Real the tested seam (repository); doubled the non-deterministic and external.
    return BookingService(Calendar(), FixedClock(CLOCK),
                          StubPaymentGateway(ok=True), SpyEmailSender(), repo)


def test_book_then_get_persists_the_booking(repo):
    service = make_service(repo)
    booking = service.book(FOCUS, ANA, START, END)
    saved = repo.get(booking.id)
    assert saved.price_cents == 6000
    assert saved.start == START
    assert saved.status == "confirmed"


def test_book_cancel_get_full_flow_against_real_sqlite(repo):
    service = make_service(repo)
    booking = service.book(FOCUS, ANA, START, END)
    refund = service.cancel(booking.id)
    assert refund == 6000
    assert repo.get(booking.id).status == "cancelled"

Real output:

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

tests/test_end_to_end_integration.py::test_book_then_get_persists_the_booking PASSED [ 50%]
tests/test_end_to_end_integration.py::test_book_cancel_get_full_flow_against_real_sqlite PASSED [100%]

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

The method diagnosis (part of the delivery)

  • What I doubled and why: the clock (FixedClock), because it's non-deterministic and I want to fix the notice that makes the refund 6000; the payment (StubPaymentGateway) and the email (SpyEmailSender), because they're external and with effects I don't want in a test. What I left real: the BookingRepository (fake and sqlite in the contract, sqlite in the integration), because it's the seam these tests exist to verify.
  • What the contract catches: the divergences in the behaviors I enumerated as clauses —if the real one stops raising on an absent id, or reconstructing the datetime, or starts duplicating—. It covers what I wrote.
  • What the integration catches: the bugs of use the contract doesn't exercise —if cancel couldn't operate on the start the repository returns—. It covers what the real flow triggers even if I didn't enumerate it.
  • How I isolated: repo fixture of scope function with :memory:, which gives a new, empty database per test and closes it when it ends; each run starts clean, without contamination between tests.

Proof of value (optional, recommended): the contract catches a breaking change

To demonstrate that the contract is worth it, change SqliteBookingRepository.get to return None instead of raising, and run the battery. It should go red exactly on clause 2, [sqlite] side:

tests/test_repository_contract.py::test_get_of_a_missing_id_raises[sqlite] FAILED [ 50%]
...
E       Failed: DID NOT RAISE KeyError
========================= 1 failed, 7 passed in 0.04s ==========================

1 failed, 7 passed: the contract catches the breaking change before the deploy, naming the clause, the provider ([sqlite]), and the symptom (DID NOT RAISE KeyError). The [fake] of that clause stays green, pointing out that the one that deviated is the real provider. That red is the demonstration that the battery isn't empty: it catches what it should catch.

Common mistakes

Delivering only the green and not the method. What happens: the three pieces are run, they come out green, and it's considered done without the diagnosis. Why it happens: the green feels like "done". How to detect it: if you can't write, for each test, why it exists and what bug it catches, you're missing the half the rubric evaluates. How to fix it: accompany the green output with the method diagnosis —what you doubled and why, what the contract catches, what the integration catches, how you isolated—. The delivery isn't the pytest output alone; it's the output plus the explanation of the decisions.

Delivering the contract one-sided. What happens: the battery is run with -k fake (or only against SQLite) and 4 passed is delivered. Why it happens: one side is faster, and the filter stayed from a previous lesson. How to detect it: if your output says deselected, you filtered; deliverable 2 asks for both sides. How to fix it: run the whole battery, without -k, and deliver the 8 passed with [fake] and [sqlite]. A one-sided contract isn't a contract —it's a test of that implementation—, and the rubric marks it as incomplete.

Doubling the tested seam or not isolating the integration. What happens: FakeBookingRepository is used in the "integration", or a database with commit is shared between tests. Why it happens: doubling everything and sharing the resource are reflexes that save effort in the short term. How to detect it: if in your integration no real piece crosses the seam, it's not an integration; if a test fails when run alongside another but passes alone, it's not isolated. How to fix it: real the repository (it's what you test), doubled the rest; and isolate with a fresh fixture or rollback so each test starts clean. They're two of the rubric's five criteria; failing them sinks the delivery even if the tests are green.

Guide close: the eight modules

You closed the capstone; let's close the guide. The complete journey, in one sentence per module:

  1. From the unit to the integration. A green unit test can hide a broken integration, because the double lied; the isolated pieces pass, the joined ones fail.
  2. The double that lied. Divergence is the natural tendency of every double: the fake returns None where the real one raises, and the bug passes the unit test and blows up in production.
  3. Contract testing: consumer and provider. The contract is a consumer-driven behavior spec, made a parametrized battery, that both sides meet; the consumer commands.
  4. Verify the contract from both sides. The consumer's test and the provider's, the same battery against the fake and the real one, and the catching of a breaking change before deploying it.
  5. Integration of real components together. BookingService + real SqliteBookingRepository, crossing the seam; what to keep real and what to double; the flow that catches what the inspection doesn't see.
  6. Real boundaries: DB, files, HTTP. Testing at the boundary —a transaction, a file, a call to a stdlib http.server— fast and deterministic.
  7. Data and isolation in integration. The rollback and the real-resource fixtures so each test starts clean; the fragility of shared real state.
  8. Capstone. The complete process over Reservo: consumer-driven contract + verification from both sides + isolated end-to-end integration, evaluated by the method.

The idea that runs through the eight, and the ecosystem's tagline: knowing which tests are worth it. A test is worth it when it tests something that can really fail and tests it in a way that, if it fails, you find out early and with precision. A contract is worth it because it keeps your doubles honest; an integration is worth it because it verifies the real collaboration. You know which one to write, when, and why —which is exactly what separates a suite that gives confidence from one that gives false confidence—.

Where to continue

This guide leaves deliberate boundaries, and each is the starting point of a sister guide in the Testing ecosystem:

  • To test a complete web app —a framework like FastAPI, routes, the request-response cycle, end-to-end HTTP— continue with testing-backend-applications-guide. Here you worked with in-process services and a SQLite repository; there you test the whole application. It's the natural next step: you bring the tested seam, it teaches you to test the app.
  • If something about stubs, spies, mocks, or fakes felt shaky, test-doubles-and-test-data-guide is the sister guide where the doubles we assumed and verified here are built. It's the foundation everything you did with the FakeBookingRepository starts from.
  • For the fundamentals and TDD —writing the test first, the red-green-refactor cycle, pytest from scratch— there's testing-fundamentals-and-tdd-guide, the base this guide stands on.
  • To take your tests beyond the examples —generating cases automatically with property-based testing, invariants, Hypothesis— continue with property-based-and-advanced-testing-guide. Where here you wrote clauses by hand, there you learn to make the machine invent the cases that would break your contract.
  • When a test goes red and you don't know why —reading a traceback, isolating the cause, distinguishing a bug in the code from a bug in the test— test-failure-diagnosis-guide is the sister guide that teaches you to diagnose the red you learned to provoke on purpose here.

With this you close contract and integration testing. You know how to build a contract that keeps your doubles honest, test the real collaboration of your components, isolate those tests so they're reliable, and use the contract as the deploy's safety net. Above all, you know how to decide: what to double, what to test real, what deserves a contract and what an integration —and why—. That's the skill the guide promised, and the one you take with you.

Resources