API Testing / Basic: trustworthy API observations / Lesson 4
Write assertions that explain the broken promise
Choose exact, partial, ordered, and unordered comparisons deliberately rather than snapshotting everything.
On this page
Lesson goals
- Match comparison strength to the documented response semantics.
- Produce focused failure evidence without normalizing away defects.
Choose what equality means
Equality is a design decision in an API test. A book identifier should usually match exactly, while a documented unordered collection may be compared without order. A sorted endpoint requires order assertions and an explicit tie-breaker. Optional fields can require a partial comparison, but partial does not mean permissive about required values. Write the comparison rule beside the contract so a reviewer knows why differences matter. Exact whole-body equality is strong only when every included field is intentionally stable; otherwise it can turn harmless presentation changes into distracting failures.
Preserve multiplicity when order is irrelevant
Sorting values or using a multiset can express unordered equality while retaining duplicate counts. Converting to a set removes multiplicity and can hide duplicate entries. In the example, the first observed collection contains the correct values in another order, while a second collection repeats one identifier. Counter preserves that distinction. Before adopting this comparison, confirm that the endpoint actually allows arbitrary order. If the endpoint promises alphabetical order, an unordered assertion would mask a real contract violation. The program evaluates local lists and performs no retrieval.
from collections import Counter
expected = ["book-2", "book-4"]
reordered = ["book-4", "book-2"]
duplicated = ["book-2", "book-4", "book-4"]
assert Counter(expected) == Counter(reordered)
assert Counter(expected) != Counter(duplicated)
print("unordered multiplicity checked")
unordered multiplicity checked
The complete example checks multiset equality for hashable string identifiers. It does not validate book objects or a server sort implementation. Use field-aware projections and tie-breaking checks when the actual contract requires them.
Bound dynamic fields rather than deleting them
Generated identifiers and timestamps need meaningful assertions even when their exact values vary. Capture the identifier, verify its documented shape, and use it to check subsequent relationships. For time, inject a clock when possible or validate a documented interval with explicit timezone handling. Deleting every dynamic field before comparison can remove the only evidence connecting response and state. The second example uses an injected integer clock for a fictional expiry rule. Its arithmetic is a policy model; it does not test wall-clock precision, clock skew, timezone parsing, or storage behavior.
def expiry(now, lifetime):
return {"issuedAt": now, "expiresAt": now + lifetime}
observed = expiry(1000, 30)
assert observed["issuedAt"] == 1000
assert observed["expiresAt"] - observed["issuedAt"] == 30
assert observed["expiresAt"] > observed["issuedAt"]
print("expiry relation accepted")
expiry relation accepted
This program checks an exact thirty-unit lifetime using a controlled clock. The units belong to the fictional contract. No credential or real token is created, and the code supplies no evidence about authentication expiry.
Make failures local and readable
Prefer named assertions that identify the violated property over one enormous boolean expression. If owner, count, and order are all checked together, a failure should still say which claim differs. Include the minimal redacted actual value and a stable case name. A snapshot can be useful for a reviewed representation, but blindly updating it converts unexpected behavior into the new expectation. Review the semantic difference first. Sensitive headers, private records, and credentials do not belong in assertion messages even when they would make debugging convenient.
Check that the assertion can detect the defect
A test that passes both a correct and a deliberately wrong observation may have no useful discriminating power. Change one synthetic field, remove a required element, or add a duplicate and verify that the comparison rejects it. This is a local review of the oracle, not permission to modify a live service. Avoid asserting implementation details that the contract does not require, such as an internal helper call count, unless that interaction is itself the boundary being tested. Strong assertions are precise about externally relevant promises and honest about unobserved internals.
Common errors and fixes
A duplicate entry passes an unordered assertion
- Why it happens
- A set comparison discards multiplicity.
- How to fix it
- Use a multiset or compare count and membership separately.
- Prevent it next time
- Include a duplicate fixture when reviewing collection assertions.
Every timestamp is removed before comparison
- Why it happens
- Normalization deletes the lifetime requirement.
- How to fix it
- Inject a clock and assert the required temporal relationship.
- Prevent it next time
- Review every ignored field against business obligations.
A snapshot is refreshed without investigation
- Why it happens
- Observed output is mistaken for approved behavior.
- How to fix it
- Explain the semantic difference and review the contract before accepting it.
- Prevent it next time
- Require meaningful review of baseline updates.
Check your understanding
1. An endpoint promises sorting by title then id. Is comparing only the set of ids sufficient?
Show worked solution
No. Check membership and multiplicity, then compare the ordered sequence against the title-and-id ordering rule. Include two books with the same title so the tie-breaker is exercised. A fixture with distinct titles alone cannot reveal a missing secondary sort.
2. A generated entry id is unknown before creation. How can the test remain precise?
Show worked solution
Validate the documented identifier representation, capture the returned value, and require the resulting state or follow-up fixture to use that same value. Also reject missing or empty identifiers. This preserves the response-to-state relation without inventing a fixed id for a genuinely generated field.
Key takeaways
- Choose equality according to documented semantics.
- Normalization must preserve the property under test.
- Review whether an assertion rejects a deliberately wrong observation.