API Testing / Intermediate: interacting contracts / Lesson 11
Test conditional updates and lost-update prevention
Model two editors and a precondition so concurrency expectations are explicit and reproducible.
On this page
Lesson goals
- Explain stale-write rejection and representation validators.
- Distinguish a sequential policy model from atomic concurrent enforcement.
Describe the interleaving before choosing a fix
Two editors can read the same note, prepare different changes, and write in sequence. Without a precondition, the second write may overwrite the first without noticing. A useful test names this interleaving explicitly: both reads observe version one, editor A writes version two, and editor B attempts a write based on version one. Decide whether the contract rejects the stale write, merges fields, or uses another reviewed policy. A single happy-path update cannot reveal this loss, even if its response and schema are correct.
Keep validators opaque and scoped
An ETag is a representation validator, not a numeric version that clients should increment themselves. If-Match uses strong comparison under HTTP semantics, so weak validators are not interchangeable with strong ones for that condition. A service must define which representation the validator identifies and how supported writes use it. The non-runnable message example shows the intended precondition shape with synthetic values. It does not demonstrate actual HTTP parsing, validator generation, response status, or intermediary behavior. Do not remove quotation marks merely because the inner text looks like a version number.
PUT /notes/note-4 HTTP/1.1
Host: catalog.example.invalid
Content-Type: application/json
If-Match: "note-v1"
{"text":"Revised classroom note"}
This protocol fragment is not sent or parsed by a server. The host is reserved example data and the validator is fictional. Actual method semantics and refusal mapping must follow the approved service contract and HTTP implementation.
Make the stale-write check deterministic
A local state machine can make the expected decision unambiguous before testing any concurrency mechanism. The program lets editor A update the fixture, then rejects editor Bβs stale expectation and preserves Aβs text. It checks both the refusal and final state. This implementation is intentionally single-process and sequential. A real persistence layer must compare and mutate atomically; checking the version in one operation and writing in another leaves a race. Passing the model therefore validates the policy example, not a database transaction or lock implementation.
note = {"version": 1, "text": "Original"}
def update(expected, text):
if note["version"] != expected:
return "STALE_WRITE"
note.update(version=expected + 1, text=text)
return "UPDATED"
assert update(1, "Editor A") == "UPDATED"
assert update(1, "Editor B") == "STALE_WRITE"
assert note == {"version": 2, "text": "Editor A"}
print("stale update rejected")
stale update rejected
The complete program checks one specified interleaving with integer fixture versions. It is not an ETag parser and has no atomic cross-process guarantee. No database, thread, service, or administrator operation is used.
Cover missing and invalid conditions
A contract that requires preconditions should define missing, malformed, stale, and current cases. Include an unchanged resource, a changed resource, and deletion between read and write if supported. Keep authorization separate: possessing a current validator does not grant permission to modify a note. Verify that a failed condition does not enqueue work or mutate derived state. If the application deliberately permits unconditional writes, document that choice rather than silently assuming every update needs the same rule. Protocol and business policy must be reviewed together.
Assign atomicity to the right evidence
Actual race testing should control the pause between read and commit in an authorized disposable environment, not rely on accidental timing or large request bursts. Establish which participant committed, which was refused, and which state and effects remain. Repeat only when a changed implementation or unresolved scheduling concern justifies it. Record the storage engine, isolation assumptions, and mechanism under test without claiming universal behavior. The local model is valuable because it defines the expected outcome clearly, while the integration test answers whether the chosen mechanism enforces that outcome atomically.
Common errors and fixes
Both stale editors overwrite successfully
- Why it happens
- The update has no effective compare-and-mutate boundary.
- How to fix it
- Use the reviewed atomic conditional-write mechanism.
- Prevent it next time
- Include a controlled two-reader interleaving regression case.
A client increments an ETag by itself
- Why it happens
- An opaque validator is treated as an application counter.
- How to fix it
- Store and return the exact documented validator representation.
- Prevent it next time
- Test quoting, weak/strong distinctions, and representation scope.
A current validator bypasses permission
- Why it happens
- Concurrency control is confused with authorization.
- How to fix it
- Check trusted subject permission independently.
- Prevent it next time
- Include denied-subject cases with otherwise current conditions.
Check your understanding
1. Editor A succeeds and editor B receives stale-write. What final observation is required?
Show worked solution
The resource must contain Aβs accepted change and the expected new version, with no B-specific side effect. A refusal message alone is insufficient if Bβs text was written before the check. Capture the final protected state and any contractually relevant event record.
2. Does passing the sequential Python model prove a real store prevents races?
Show worked solution
No. It establishes the chosen policy for one deterministic interleaving. The store must perform comparison and mutation atomically under its actual concurrency model. Verify that mechanism separately with controlled participants in an approved isolated environment and record the assumptions.
Key takeaways
- A concurrency test starts with a named interleaving and expected final state.
- Validators do not grant permission.
- Sequential models define policy; atomic enforcement needs separate evidence.