Unit 09.03: Asserting on the contract, not the wording
Assert the contract from Module 1, not the exact response you happened to get.
Allowed values, ranges, required fields, no extras
A response checked against five contract properties instead of an exact match.
The code compares both approaches.
RESPONSE = {"category": "billing", "confidence": 0.87,
"request_id": "r-8841"}
brittle = RESPONSE == {"category": "billing", "confidence": 0.9,
"request_id": "r-8841"}
contract = [
("status is 200", True),
("category is one of the four", RESPONSE["category"] in
{"billing", "technical", "account", "other"}),
("confidence is a float in range", 0 <= RESPONSE["confidence"] <= 1),
("request_id is present", "request_id" in RESPONSE),
("no unexpected fields", set(RESPONSE) <= {"category", "confidence",
"request_id"}),
]
print(f"exact-match assertion: {brittle} <- fails on any confidence change")
for name, ok in contract:
print(f" {'PASS' if ok else 'FAIL'} {name}")
# Assert the contract from Module 1: allowed values, ranges, required fields,
# and no extras. The last check is the one that catches an accidental leak of
# an internal field.
The exact match fails on a confidence of 0.87 against 0.9 - a difference nobody cares about. The five contract checks all pass and each names something a caller relies on.
The last check is the one worth copying: asserting no *unexpected* fields catches an internal field accidentally leaking into a response, which is the failure the response model exists to prevent.
The mistake this prevents
The mistake is asserting on the full response dictionary. It breaks every time anyone adds a field, including deliberately, so the test becomes noise and eventually gets loosened to nothing.
Takeaway
Assert allowed values, ranges, required fields and the absence of unexpected ones. Exact-response assertions fail on harmless changes.
