Module 8: Project Contract And Integration For Reservo
4. Verify the contract against the real `SqliteBookingRepository`
Description
Here the second deliverable closes. In the previous lesson you ran the contract against the fake and saw four greens that, you learned, measured coherence with yourself, not coincidence with the real thing. Now you bring the external reference: the SqliteBookingRepository, the piece that actually runs in production. You run the complete battery —without a filter, the eight cases— and see [fake] and [sqlite] side by side, the four clauses green in both implementations. That's the moment your contract stops being a self-exam and becomes a certification: the fake and the real one, verified against the same spec, meet the same thing.
And there's a logical argument behind that green worth naming, because it's the heart of why the contract works: transitivity. If the fake meets the contract, and the real one meets the contract, then the fake and the real one coincide in everything the contract covers —not because you compared them directly against each other, but because both were measured against the same yardstick—. There aren't two specs that could diverge: there's a single one, run twice. That's the technical guarantee that turns "I hope my fake doesn't lie" into "my fake can't lie about anything the contract covers without a red test giving it away". In this lesson you see it work, with real output, and you understand why "one battery, two providers" is the way —and not two twin suites.
Connection to the module: this lesson completes deliverable 2 and prepares deliverable 3. With the contract green on both sides, you have the first layer of the guarantee: each clause you enumerated is verified against the fake and against the real one. What the contract does not cover —the bugs of use you didn't enumerate— is what lesson 5's end-to-end integration catches. And today's green contract is also the one that, in lesson 7, will go red when someone breaks a promise: the safety net that's assembled here. Today you see it in its healthy state; later you'll see it catch.
Analogy: two clocks against the official time
You have two clocks: the one in your kitchen (cheap, convenient, always in sight) and the one at the train station (the one that really rules when your train leaves). You want to be sure you can trust the kitchen one so as not to miss the train. A clumsy way would be to compare the two clocks directly every morning, face to face —impossible, they're in different places—. The good way is for each one to set itself against the official time: the time signal from the national observatory, the same for both. If the kitchen clock shows the official time, and the station clock shows the official time, then the two show the same —without ever having compared them to each other—. The official time is the common yardstick, and the coincidence between the clocks follows from both meeting it.
The contract is the official time. The FakeBookingRepository is the kitchen clock —the one you use all day in your fast unit tests— and the SqliteBookingRepository is the station clock —the one that rules in production—. You don't compare the fake against the real one directly, object by object; you set both against the same battery of clauses. When lesson 3 showed [fake] in green, the kitchen clock was showing the official time. When this lesson shows [sqlite] also in green, the station clock is showing it too. The conclusion is transitive and solid: they show the same, so you can trust the kitchen one so as not to miss the train. And if one day one went off —the fake that returns None, the real one that doesn't serialize—, it would stop showing the official time, and its clause would go red, pointing at it. The common yardstick is what makes it possible to trust the convenient clock.
Worked example: the eight cases, fake and real, in green
Let's run the complete battery, without -k, so pytest executes the eight cases: four clauses times two providers. This is the delivery's command —that's how a contract is run, with both sides together—:
What to expect. On my machine (Python 3.14.0, pytest 9.1.1):
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 ==============================
Eight greens. Read them as an inspection report: each clause passed both [fake] and [sqlite]. That's the contract's complete certification —the second deliverable, finished—: the fake and the real one behave the same in the four clauses, verified, not assumed. And notice the economy you already saw in module 3: you didn't write eight tests, you wrote four; the parametrized fixture doubled each one. Compare this result with lesson 3's 4 passed, 4 deselected. There you had half a contract —the kitchen clock against the official time—; here you have the whole contract —the two clocks against the same yardstick—. The difference between the two commands is one -k fake less, and it's the whole difference between "coherent with me" and "coincides with the real thing".
Pause a second on clause 1, test_save_then_get_returns_the_same_booking, because its [sqlite] green is the one that says most. That clause compares the recovered booking with the saved one, field by field, including the start. That it passes against [sqlite] means that SqliteBookingRepository.get reconstructs the datetime on read —it converts the ISO text back with datetime.fromisoformat—, because if it returned the start as a str, a Booking with a text start wouldn't be equal to one with a datetime start, and the case would be red. The [sqlite] green on that clause is the proof that the datetime bug the guide pursued since module 1 is closed in this provider. The contract doesn't just certify: it documents that the seam is healthy.
Why transitivity, and not the direct comparison
It's worth understanding why the contract uses a common yardstick instead of comparing the fake against the real one directly. You could imagine a test that did something like "save the same in the fake and in the real one, and compare that get returns equal objects in both". It sounds reasonable, and for simple cases it would work. But it has two problems that the contract-as-yardstick avoids.
First, the direct comparison doesn't say which behavior is the correct one. If the fake and the real one differ, which is right? The direct comparison only says "they differ"; it has no notion of what should happen. The contract does: each clause asserts the behavior the consumer needs, so when one side fails, you know that side deviated from the correct behavior, not just that the two don't coincide. The yardstick isn't just any: it's the one the consumer dictated.
Second, the direct comparison couples the two implementations. A test that saves in both and compares has to know the two providers at once and build them together; adding a third (a file-based repository) forces you to rewrite the comparison. With the common yardstick, each provider is measured separately against the same spec: adding one is a word in params, and its coincidence with the others follows by transitivity, without writing a single new comparison. The yardstick scales; the face-to-face comparison doesn't.
That's why "one battery, two providers" —one, not two twin suites, and not a direct comparison— is the canonical way. There's one spec. If a clause changes, it changes for both at once, because it's the same function. If a provider stops meeting it, its run goes red and the id in brackets names it, while the other stays green. The only way for the fake to "pass as good" is for it to really meet the four clauses, just like the real one —and that's exactly what you wanted to guarantee—.
What the green contract guarantees, and what it doesn't
With deliverable 2 complete, it's worth being precise about what you bought with these eight greens, because it marks the boundary with deliverable 3.
What it guarantees: that the fake and the real one coincide in the four clauses you enumerated. Any divergence in a behavior you wrote as a clause will pop up in red. If tomorrow the real one stops raising on an absent id, or stops reconstructing the datetime, or starts duplicating instead of updating, the contract catches it. That's a lot: it covers the known divergences, the ones experience taught you to fear.
What it doesn't guarantee: that the fake and the real one coincide in something you did not write as a clause. The contract has exactly the gaps you leave it. If you forgot a clause about how find_by_room behaves with cancelled bookings, that difference lives in the gap, invisible to the green contract. And —more important for what comes next— the contract inspects the shape of the data field by field; it doesn't exercise the use BookingService makes of them in a live flow. A bug that only appears when cancel uses the start in a subtraction can hide in a contract that verified the start with == but that, if it had a gap right there, wouldn't see it.
That second limitation is the reason deliverable 3 exists. The contract certifies each piece against the spec, clause by clause; the end-to-end integration verifies that the pieces, used for real in a flow, collaborate —and catches the bugs of use that no clause enumerated—. With the contract green on both sides, you have the first layer. Lesson 5 adds the second.
Common mistakes
Running the contract with -k and believing it's the delivery. What happens: out of the habit from lesson 3, someone always runs with -k fake or -k sqlite and delivers a single side. Why it happens: the filter stayed from the previous step. How to detect it: if your output says deselected, you filtered; the contract's delivery does not filter. How to fix it: the contract's battery is run whole, without -k, to see [fake] and [sqlite] together. The 4 passed of one side isn't the contract; the 8 passed of both is.
Confusing "both green" with "both identical in everything". What happens: on seeing the eight greens, someone concludes the fake and the real one are interchangeable for any purpose. Why it happens: "they meet the same contract" feels like "they're the same". How to detect it: the fake and the real one coincide in what the contract covers, not necessarily in the rest —speed, disk persistence, behavior outside the clauses—. How to fix it: remember that the contract guarantees coincidence in its clauses, no more and no less. For what the contract doesn't cover, there are still real differences (the real one persists, the fake doesn't; the real one is slower), and that's why deliverable 3 tests the real flow and doesn't settle for the contract.
Suspecting the contract when both sides fail. What happens: one day a clause fails on [fake] and [sqlite] at once, and someone looks for the bug in the two providers. Why it happens: two reds look like two bugs. How to detect it: it's rare for two independent implementations to break the same way at once; when both sides of the same clause fail, the problem is most likely in the test or in the contract itself —a badly written assertion, a wrong sample datum—, not in the providers. How to fix it: when a single side fails, suspect that provider (it deviated from the contract); when both fail, suspect the clause. The asymmetry of the red is diagnostic; read it before touching code.
Exercises
Exercise 1 — The transitivity argument, written. State, in the form of a syllogism, why the contract green on both sides guarantees that the fake and the real one coincide, without having compared them directly. Then say what would happen to the argument if the contract had only three clauses instead of four.
See solution
The syllogism:
- The
FakeBookingRepositorymeets the four clauses of the contract ([fake]in green showed it). - The
SqliteBookingRepositorymeets the four clauses of the contract ([sqlite]in green showed it). - Therefore, the fake and the real one coincide in the behavior the four clauses describe —without having compared them against each other, because both were measured against the same yardstick—.
With only three clauses: the argument would still be valid, but it would guarantee less. The conclusion would be "the fake and the real one coincide in the behavior the three clauses describe". The behavior the fourth clause covered —say, find_by_room returns only that room's bookings— would fall outside the guarantee: the fake and the real one could diverge there without any red giving it away. Transitivity only covers what the yardstick measures. That's why the guarantee's reach is exactly the set of clauses: each clause you remove is a door you leave open, and each one you add (driven by a real need) is one you close.
Exercise 2 — Which clause proves the datetime is fixed. Of the four clauses, which one fails against [sqlite] if SqliteBookingRepository.get returned the start as a str (the module 1 bug, without the fromisoformat fix)? Explain why, and why that same clause passes against [fake].
See solution
test_save_then_get_returns_the_same_booking[sqlite] fails, clause 1. That clause does repo.save(booking) and then assert repo.get("bk-1") == booking, comparing the whole object. The dataclass comparison compares field by field, including the start. If get returned the start as a str —'2026-03-10T09:00:00'— while the saved booking has it as datetime(2026, 3, 10, 9), the two objects wouldn't be equal (str != datetime in that field), and the assertion would fail with [sqlite].
Why it passes against [fake]: the FakeBookingRepository saves the whole object in a dict and returns it identical —it never serializes anything—, so the start comes back as the original datetime and the comparison is datetime == datetime, true. The clause passes against the fake and would fail against the buggy real one: exactly the asymmetry the contract exists to exhibit. That in the real delivery both sides are green is the proof that the SqliteBookingRepository does reconstruct the datetime (with fromisoformat), closing the bug.
Exercise 3 — Direct comparison versus common yardstick. A colleague proposes replacing the contract with a single test that saves a booking in the fake and in the real one and compares that both get return equal objects. Give two concrete reasons why the common yardstick (the contract) is better, using the case of adding a third provider (a file-based repository).
See solution
Reason 1 — the common yardstick says what's correct; the direct comparison doesn't. If the direct-comparison test found that the fake and the real one differ, it would only say "they differ"; it wouldn't know which is right. The contract asserts the behavior the consumer needs, so when one side fails, you know that that side deviated from the correct thing. On adding the file-based repository, the contract measures it against the same consumer standard; the direct comparison would have to arbitrarily decide against which of the two existing ones to compare it.
Reason 2 — the common yardstick scales; the direct comparison couples. Adding the file-based repository to the contract is adding "file" to params: one word, and the four clauses certify it, with its coincidence with the other two following by transitivity. With the direct comparison you'd have to write new comparisons —fake against file, real against file—, because comparing pairwise isn't transitive in the code: each new provider multiplies the pairs you have to write. The common yardstick turns "N providers coincide" into "each one meets the contract", which is linear; the direct comparison is quadratic in pairs.
In one sentence: the common yardstick has a notion of the correct thing (the consumer) and measures each provider separately; the direct comparison lacks that notion and couples the providers to each other. That's why the contract is the canonical way.
Summary and next step
In this lesson you closed the second deliverable: you ran the complete battery —without a filter— and saw the eight cases in green, [fake] and [sqlite] for each of the four clauses. That green on both sides turns your contract from a self-exam into a certification: the fake and the real one, measured against the same spec, meet the same thing. With the two clocks against the official time you understood transitivity —if both show the official time, they show the same, without having compared them to each other— and why the common yardstick is better than the direct comparison: it has a notion of the correct thing (the consumer) and scales to more providers with a word. And you pinned down the reach: the contract guarantees coincidence in the clauses you enumerated, not in what you didn't write nor in the live use of the data.
Before moving on you should be able to: run the whole contract and read the eight cases; state the transitivity argument and its reach; and explain why "one battery, two providers" beats the direct comparison between implementations.
With the contract green you have the first layer of the guarantee: the divergences you enumerated are covered. The second is missing, the one that covers what the contract doesn't see —the bugs of use that only appear when the pieces work together in a real flow—. In lesson 5 you build the third deliverable: the end-to-end integration test, BookingService + real SqliteBookingRepository, exercising book→get→cancel for real, crossing the seam as it will in production.
Resources
- pytest documentation — Parametrizing fixtures and test functions — the mechanism that runs the same battery against the fake and the real one; the technical basis of "one battery, two providers" and of transitivity.
sqlite3—connectand in-memory databases (Python documentation) — howsqlite3.connect(":memory:")creates the ephemeral, clean-per-test database that acts as the real provider on the[sqlite]side of the contract.datetime.fromisoformat— Python documentation — the functionSqliteBookingRepository.getuses to reconstruct thedatetime, and the reason clause 1 passes against[sqlite].- docs.pact.io — Provider verification — the industry version of running the same contract against the real implementation; the pattern you run here by hand against the
SqliteBookingRepository.