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 book→get→cancel. 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:
booksaves,cancelre-reads); getof an absent id raises (need:cancelmust find out if the booking doesn't exist);- saving the same id twice updates, doesn't duplicate (need:
cancelrewrites the cancelled booking); find_by_roomreturns only that room's bookings (need:bookchecks 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 book→get→cancel, 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.
| Criterion | What's sought | Sign that it's right |
|---|---|---|
| Decision | You chose what to double and what to keep real, and you know how to justify it | Real the repository (the tested seam); doubled clock, payment, and email (non-deterministic, external), with the reason stated |
| Consumer-driven contract | The clauses come from real needs of BookingService, not from the provider's catalog | Each clause can be traced to a line of book or cancel; none mentions internal SQLite details |
| Both sides | The contract runs against the fake AND the real one, with a single battery | 8 passed with ids [fake] and [sqlite], not two twin suites nor a single side |
| Isolated integration | The real flow crosses the seam and each run starts clean | The flow book→get→cancel green, with fixture or rollback; it doesn't contaminate between tests |
| Explanation | You can say, for each piece, why it exists and what bug it catches | A 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 refund6000; 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: theBookingRepository(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
cancelcouldn't operate on thestartthe repository returns—. It covers what the real flow triggers even if I didn't enumerate it. - How I isolated:
repofixture of scopefunctionwith: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:
- 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.
- The double that lied. Divergence is the natural tendency of every double: the fake returns
Nonewhere the real one raises, and the bug passes the unit test and blows up in production. - Contract testing: consumer and provider. The contract is a consumer-driven behavior spec, made a parametrized battery, that both sides meet; the consumer commands.
- 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.
- Integration of real components together.
BookingService+ realSqliteBookingRepository, crossing the seam; what to keep real and what to double; the flow that catches what the inspection doesn't see. - Real boundaries: DB, files, HTTP. Testing at the boundary —a transaction, a file, a call to a stdlib
http.server— fast and deterministic. - Data and isolation in integration. The rollback and the real-resource fixtures so each test starts clean; the fragility of shared real state.
- 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-guideis the sister guide where the doubles we assumed and verified here are built. It's the foundation everything you did with theFakeBookingRepositorystarts 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 withproperty-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-guideis 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
- pytest documentation — Parametrizing fixtures and test functions — the central mechanism of deliverable 2: one battery, two providers, without duplicating code.
sqlite3— DB-API for SQLite (Python documentation) — the real resource the contract certifies and the integration crosses;connect(":memory:")gives the ephemeral, isolated database of the delivery.- docs.pact.io — Consumer-driven contracts — the industry tool that automates and networks what you assembled here by hand; the destination when the seam is between services and not in-process.
testing-backend-applications-guide— the sister guide the path continues to: testing a real web application, with a framework, routes, and end-to-end HTTP, the other side of the boundary this guide leaves marked.