API Testing / Intermediate: interacting contracts / Lesson 9
Test pagination, filtering, and stable order
Use exact collection membership and tie-breakers to detect gaps, duplicates, and filter drift.
On this page
Lesson goals
- Separate filter correctness, ordering, page boundaries, and traversal behavior.
- Explain the limits of static fixtures when collections change.
Specify the collection pipeline
A collection endpoint combines several contracts: which records are visible, which filters apply, how matching records are ordered, and how that order is split into pages. Test them separately before composing them. An alphabetical list requires a secondary key when titles tie; otherwise different executions can move equal-title records across page boundaries. Define whether total means all authorized records or only filtered records, and whether it is exact or approximate. Do not infer these meanings from a field name. The classroom dataset is fixed, small, and entirely local.
Exercise the ordering tie-breaker
Fixtures with only distinct titles cannot detect a missing secondary sort. Include equal titles with different stable identifiers and place the tie near a page boundary. The first program filters a category and orders by title then id. It asserts the exact ordered identifiers, not merely their set. The displayed ordering is a fictional code-point-based classroom rule. Locale-aware collation, normalization, case handling, and database-specific comparison rules can produce different results and need an explicit contract plus tests in the selected environment.
books = [{"id":"b3","title":"Atlas","kind":"essay"}, {"id":"b1","title":"Atlas","kind":"essay"}, {"id":"b2","title":"Brook","kind":"poem"}, {"id":"b4","title":"Cedar","kind":"essay"}]
matching = [b for b in books if b["kind"] == "essay"]
ordered = sorted(matching, key=lambda b: (b["title"], b["id"]))
assert [b["id"] for b in ordered] == ["b1", "b3", "b4"]
print("filtered order accepted")
filtered order accepted
The complete program tests three matching synthetic records and one excluded record. Python sorting is actually used locally. No claim is made about database collation, query planning, or an HTTP endpoint.
Check the whole bounded traversal
One correct first page can hide a broken continuation. For a fixed fixture, concatenate all pages, require every expected identifier exactly once, and verify that the final page terminates according to the documented signal. Include empty, exactly-full, and partial-final-page cases. A cursor should be treated as opaque by clients unless its structure is explicitly part of the contract. The second example checks local offset slices only. It does not implement a cursor, validate signed state, or prove that a changing server-side collection is traversed consistently.
ids = ["b1", "b3", "b4", "b8", "b9"]
size = 2
pages = [ids[offset:offset + size] for offset in range(0, len(ids), size)]
joined = [item for page in pages for item in page]
assert pages == [["b1", "b3"], ["b4", "b8"], ["b9"]]
assert joined == ids
assert len(joined) == len(set(joined))
print("three-page traversal accepted")
three-page traversal accepted
The program verifies page boundaries, ordering, and duplicate absence for this five-item fixture. It uses no network. Static offset slicing supplies no evidence about inserts, deletes, snapshot isolation, or provider continuation tokens.
Preserve filter meaning in continuations
A next-page link or token must preserve the intended query scope. Test that category, sort direction, tenant scope, and supported page-size rules stay consistent throughout traversal. Empty filters, repeated query parameters, spaces, and non-ASCII text need documented serialization rules. Construct query strings with a supported encoder rather than concatenating raw user text. Parsing a URL locally only checks representation; it does not authorize following an arbitrary link. An actual client should apply its approved destination and credential rules, especially when a continuation includes an absolute URL.
Declare what happens while data changes
Offset pagination can skip or repeat records if earlier rows are inserted or removed between requests. Cursor pagination also has tradeoffs and does not automatically guarantee a stable snapshot. Choose and document the consistency promise: best-effort live view, snapshot traversal, or another explicit model. Then test controlled mutation interleavings in an approved disposable environment. Keep the static tests because they isolate simpler defects, but do not present them as proof of concurrent traversal correctness. Counts, ordering, and authorization changes may each require separate expectations under the chosen model.
Common errors and fixes
Page two repeats the last item on page one
- Why it happens
- The continuation boundary or comparison operator is wrong.
- How to fix it
- Use a fixture with ties at the boundary and compare the full traversal.
- Prevent it next time
- Require no gaps or duplicates under the documented consistency model.
Total ignores the active filter
- Why it happens
- The count query and data query use different predicates.
- How to fix it
- Compare count semantics with exact filtered membership.
- Prevent it next time
- Review shared scope and filter definitions for both observations.
Order tests pass despite unstable title ties
- Why it happens
- Every fixture has a unique title.
- How to fix it
- Add equal-title records and an explicit stable secondary key.
- Prevent it next time
- Exercise tie-breakers near page boundaries.
Check your understanding
1. Five matching records use page size two. What should a fixed-data test expect?
Show worked solution
Expect pages of two, two, and one, the documented termination signal, and exactly five distinct expected identifiers in the correct order. Also verify excluded records never appear. This establishes the fixed fixture behavior; it does not specify how concurrent insertions should behave.
2. Why can an unordered membership assertion miss a pagination defect?
Show worked solution
It may confirm that each page contains plausible records while losing the global sequence and multiplicity. Compare concatenated pages with the complete expected ordered fixture. Add tied sort values and boundary cases so a defective continuation cannot pass simply by returning familiar identifiers.
Key takeaways
- Test visibility, filtering, ordering, and page splitting separately and together.
- Whole-traversal checks reveal gaps and duplicates.
- Static fixtures cannot establish concurrent collection consistency.