Module 4: Verifying The Contract From Both Sides

3. The provider side

Description

Switch chairs. In lesson 2 you looked at the seam from BookingService, the consumer that uses the repository and asks "am I relying only on what's promised?". Now you sit on the other side, in the provider: the component that implements the collaborator. At the repository seam, the provider is the real SqliteBookingRepository (and also the fake, which is another provider of the same seam). Its point of view is the consumer's mirror question: "given X, I return Y". Given that I'm asked to save a booking and then read it, I return that same booking, with the correct types. Given that I'm asked for an id that doesn't exist, I raise. Given that the same id is saved to me twice, I update without duplicating.

The provider test verifies that the implementation fulfills each clause of the contract, isolated from the consumer. There's no BookingService in sight: you don't need a service, or a calendar, or a payment, to ask the repository whether it honors its contract. You talk to it directly —repo.save(...), repo.get(...)— and check that it responds according to what was agreed. And here it does touch the real piece: while the consumer was tested against the fake (fast, in memory), the real provider is exactly what needs to be verified, because it's the one that has to serialize the datetime, write to the table, enforce the PRIMARY KEY. The provider test is where SQLite answers for itself.

Connection to the module: this lesson closes the pair lesson 1 announced. With lesson 2 (consumer) and lesson 3 (provider) you have the two chairs. Lesson 4 will show that these two sides aren't two different batteries, but the same battery run against the two providers, and why running it against both is the guarantee that neither lies. Here you'll see the provider test for what it is: module 3's contract battery, looked at from the side that has to pass it, and filtered to the real provider to see it answer for itself alone.

Analogy: the factory inspection against the standard

Go back to the plug and the outlet from lesson 1, but now enter the outlet factory. There are no lamps there: there's an inspector with a meter and a copy of the electrical standard. Their job is to take an outlet fresh off the line and verify it, point by point, against the standard: "does it deliver 120 volts? yes. are the prongs at the standard distance? yes. is the ground where the standard requires it? yes". They don't need to plug in any lamp to do their job; the product and the standard are enough. If the outlet fulfills each clause, it passes; if it delivers 240 volts, it's rejected right there, in the factory, before it reaches a wall.

That inspector is the provider test. The standard is the contract. The outlet is the SqliteBookingRepository. The inspector asks it the contract's questions directly —save and return, missing id, double save, filter by room— and verifies that each answer complies. It doesn't start BookingService (it doesn't plug in a lamp) because it doesn't need to: the question "does this provider fulfill the standard?" is answered with the provider and the standard, nothing more. And notice the contrast with lesson 2: the lamp's manufacturer tested the lamp against the standard (the consumer test); the outlet's manufacturer tests the outlet against the same standard (the provider test). Two inspections, two products, a single standard —and neither of the two needs the other's product—.

Worked example: the real provider answering for itself

The provider test is module 3's contract battery, looked at from the SqliteBookingRepository's side. Since the battery is parametrized with params=["fake", "sqlite"], the "real provider test" is simply that battery filtered to the sqlite provider: the same four clauses, run only against the real implementation, to see it answer for itself alone. Let's recall the battery (it's the same as lesson 1's):

# tests/test_repository_contract.py — the four clauses, against each provider
def test_save_then_get_returns_the_same_booking(repo):
    repo.save(a_booking())
    got = repo.get("bk-1")
    assert got.id == "bk-1"
    assert got.room_id == "focus"
    assert got.start == START            # datetime, not str  <-- SQLite has to serialize and reconstruct
    assert got.price_cents == 6000
    assert got.status == "confirmed"


def test_get_of_a_missing_id_raises(repo):
    with pytest.raises(KeyError):
        repo.get("does-not-exist")       # <-- SQLite has to raise, not return None


def test_saving_the_same_id_twice_updates_not_duplicates(repo):
    repo.save(a_booking(status="confirmed"))
    repo.save(a_booking(status="cancelled"))
    got = repo.get("bk-1")
    assert got.status == "cancelled"     # <-- SQLite's ON CONFLICT ... DO UPDATE
    assert len(repo.find_by_room("focus")) == 1


def test_find_by_room_returns_only_that_rooms_bookings(repo):
    repo.save(a_booking(id="bk-1", room_id="focus"))
    repo.save(a_booking(id="bk-2", room_id="studio"))
    ids = {b.id for b in repo.find_by_room("focus")}
    assert ids == {"bk-1"}               # <-- SQLite's WHERE room_id = ?

Now we run it filtered to the real provider, with pytest's -k sqlite selector, which only runs the tests whose id contains sqlite:

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

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

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[sqlite] PASSED [ 50%]
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[sqlite] PASSED [100%]

======================= 4 passed, 4 deselected in 0.01s ========================

Four greens, and the other four (the [fake]) deselected —pytest collected them but didn't run them, because their id doesn't contain sqlite—. This is the provider answering for itself alone: the real SqliteBookingRepository fulfills the four clauses of the contract, without BookingService in between. Each green is a promise fulfilled by the real implementation: test_save_then_get_returns_the_same_booking[sqlite] verifies that SQLite serializes the datetime to text in save and reconstructs it to datetime in get (the got.start == START passes); test_get_of_a_missing_id_raises[sqlite] verifies that a SELECT with no rows raises KeyError instead of returning None; test_saving_the_same_id_twice_updates_not_duplicates[sqlite] verifies that the ON CONFLICT(id) DO UPDATE updates instead of duplicating; and test_find_by_room_returns_only_that_rooms_bookings[sqlite] verifies that the WHERE room_id = ? doesn't slip in bookings from another room. The real provider, passed by the standard.

The symmetry —and the asymmetry— between the two tests

Consumer and provider verify the same contract, but they're not identical images. It's worth seeing where they're symmetric and where they're not, because that's the reason both are needed.

They're symmetric in the standard. Both tests talk about the same contract: the same four clauses. The consumer relies on them ("I expect get to raise"); the provider fulfills them ("I raise"). It's literally the same agreement seen from the two sides. That's why, when in lesson 4 you run the battery against both providers, you won't be writing two contracts: you're running one contract against two implementations.

They're asymmetric in the setup. The consumer test needs the complete consumer —BookingService with all its collaborators doubled— and any provider that honors the contract (the fake, for speed). The provider test doesn't need the consumer at all: it talks directly to the implementation. One builds a service; the other builds a repository. Each side's setup is the minimum that side needs.

They're asymmetric in which piece is real. In the consumer test, the provider is doubled (the fake): what's tested for real is the consumer's logic. In the provider test, the implementation is the real one (SQLite): what's tested is the provider's fidelity. Each test puts one side under the spotlight and takes the other as good. That's why neither replaces the other: a perfect consumer doesn't guarantee a faithful provider, and a faithful provider doesn't guarantee the consumer uses it well. Only the two together cover the whole contract.

This asymmetry explains why the consumer is tested against the fake and the provider against SQLite, without it being contradictory. It's not that "some tests use the fake and others the real one" at random: it's that each side doubles what it's not testing and leaves real what it is. The consumer test doubles the provider (to isolate the consumer's logic); the provider test has nothing to double, because the provider is exactly what it examines.

Why the provider is tested against the real thing (and the consumer isn't)

Let's pause on the question that confuses most: if in lesson 2 we insisted on testing the consumer against the fake "because the fake honors the contract", why is the provider now tested against SQLite and not against the fake? The answer is direct: the provider test exists precisely to verify that the real implementation honors the contract. Testing the provider against the fake would be absurd —you'd verify that the fake fulfills the contract using the fake, a circle—. The point of the provider test is to put the implementation under suspicion to the test: the one that serializes, that writes to disk, that can diverge. That's SQLite.

Put another way: the fake is taken as good because the contract keeps it honest, and what keeps it honest is this very battery run against it. The real provider isn't taken as good; it's verified. In lesson 4 you'll see that the battery runs against both —the fake to confirm it still honors the contract, the real one to confirm it fulfills it—, and that "both" is what guarantees that the fake you used in the consumer test wasn't lying. For now, the rule: the provider test points at the provider that can fail, and that's the real one.

Common mistakes

Putting BookingService in the provider test. What happens: someone tests that SQLite fulfills the contract by going through BookingService.book, instead of calling repo.save/repo.get directly. Why it happens: the habit of testing "the whole flow" is carried over. How to detect it: if your provider test builds a Calendar, a StubPaymentGateway, and a BookingService, you're testing two things at once and no longer isolating the provider. How to fix it: talk to the repository directly. The provider test asks "does this repository fulfill the contract?", and that's answered with the repository and the contract, without a service. If book had a bug, you wouldn't want it to contaminate the verdict about SQLite.

Testing the provider against the fake. What happens: someone runs the battery only with -k fake and concludes "the provider fulfills the contract". Why it happens: you lose sight of which provider is under test. How to detect it: if your "provider test" never touches SqliteBookingRepository, you didn't verify the real implementation —you verified the fake, which you already took as honest—. How to fix it: the real provider test runs against sqlite (-k sqlite, or the complete battery that includes both). The fake is the consumer test's stand-in; the object of the provider test is the implementation that can diverge.

Believing a provider passed today stays passed forever. What happens: the four [sqlite] greens give peace of mind and the battery stops being run when someone touches SqliteBookingRepository. Why it happens: the green feels definitive. How to detect it: if the contract isn't run again after every provider change, a breaking change (lesson 5) slips in unseen. How to fix it: the provider test is worth it by running again on every implementation change. A "passed" provider that changed and wasn't re-verified is an unpassed provider. The factory inspection is done on every batch, not once.

Exercises

Exercise 1 — Translate each clause into what SQLite must do. For each of the four contract clauses, write the sentence "to pass this clause, SqliteBookingRepository has to...", naming the concrete SQLite mechanism that fulfills it.

See solution
  • Save-and-read returns the same booking: ...has to serialize the fields in save (the datetime to ISO text with .isoformat(), the rest directly) and reconstruct them in get (the text back to datetime with datetime.fromisoformat, the integers and text as-is), so that the booking that comes out equals the one that went in, field by field.
  • get of a missing id raises: ...has to detect that the SELECT ... WHERE id = ? returned no rows (fetchone() gives None) and raise KeyError in that case, instead of returning the raw None.
  • Saving the same id twice updates without duplicating: ...has to use INSERT ... ON CONFLICT(id) DO UPDATE SET ..., relying on the id column being PRIMARY KEY, so that the second save of the same id overwrites the row instead of creating a second one.
  • find_by_room returns only that room's bookings: ...has to filter with SELECT ... WHERE room_id = ?, so that a booking from another room never appears in the result.

The essence: each contract clause translates into a concrete responsibility of the implementation. The provider test is what verifies those responsibilities are met. A different provider (for example, one over PostgreSQL) would have other mechanisms —another upsert syntax— but should fulfill the same clauses. The contract is stable; the implementation varies.

Exercise 2 — The -k selector. In the worked example we ran -k sqlite and pytest reported "4 deselected". Explain what the selector did, and write the command to run, conversely, only the fake side. What would that run test and what not?

See solution

The -k sqlite selector tells pytest: "of all the collected tests, run only those whose id contains the substring sqlite". Since the battery is parametrized, each test has two variants —...[fake] and ...[sqlite]—; -k sqlite selects the four [sqlite] and deselects the four [fake] (it collects them but doesn't run them, hence the "4 deselected"). It's a way of looking at a single side of the contract without deleting the other.

The command for the fake side:

python3 -m pytest tests/test_repository_contract.py -v -k fake

That run would test that the FakeBookingRepository still fulfills the four contract clauses —useful to confirm that the consumer test's stand-in didn't desynchronize—. What it would not test is the real implementation: it says nothing about whether SqliteBookingRepository serializes correctly, raises on the missing id, or filters by room. That's why neither side alone is enough: -k fake verifies the double, -k sqlite verifies the real one, and only running the complete battery (both) gives the whole contract. The -k is for inspecting one side, not for replacing the complete run.

Exercise 3 — A second provider. Imagine Reservo adds InMemorySqliteViaFile, another real provider that saves to a SQLite file on disk instead of :memory:. What would you have to change in the contract battery to verify it as a third provider, and what wouldn't change? What would it tell you if it passes the four clauses?

See solution

What changes: only the fixture that manufactures the provider. You add a third value to params and its construction branch:

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

What does NOT change: the four clauses. Not one line of the four test_... is touched. That's the whole point of the contract: it's an implementation-independent behavior spec, so verifying a new provider is plugging it into the fixture, not rewriting the verifications. Each test would now run three times: [fake], [sqlite], [sqlite-file].

What it would tell you if it passes the four clauses: that this third provider honors the same contract as the other two, and is therefore interchangeable with them from the consumer's point of view. BookingService could use any of the three without changing a line, because all three fulfill the same promises. That guaranteed interchangeability —"any provider that passes the contract works"— is the value of having a contract instead of tying yourself to an implementation. And if the on-disk provider failed, say, the double-save clause, you'd know instantly that its upsert isn't right, without touching anything of the consumer.

Summary and next step

In this lesson you sat in the provider's chair and learned its mirror question: "given X, I return Y". The provider test verifies that the implementation fulfills each clause of the contract, isolated from the consumer —without BookingService, talking directly to the repository—. You saw it for what it is: module 3's contract battery, filtered to the real provider with -k sqlite, showing the SqliteBookingRepository answer for itself alone: the four greens that confirm it serializes and reconstructs the datetime, raises on the missing id, updates without duplicating, and filters by room. With the factory inspection against the standard you understood that this side doesn't need the other's product: the provider is verified with the provider and the standard. And you understood the symmetry (the same standard) and the asymmetry (different setup, different real piece) that make both sides necessary.

Before moving on you should be able to: write a provider test that talks directly to the repository; explain why the provider is tested against the real thing while the consumer was tested against the fake, without it being contradictory; and translate each contract clause into the SQLite mechanism that fulfills it.

What comes next joins the two chairs. Lesson 4 shows that the consumer test and the provider test don't live in separate batteries: they're the same battery run against the two providers at once, and that "at once" is the whole guarantee. You'll see why running a contract against both —not against one— is exactly what closes module 1's gap: the one that let a fake lie without anyone noticing.

Resources