Course outline

API Testing / Intermediate: interacting contracts / Lesson 12

Test asynchronous workflows with bounded observations

Model accepted work, progress, terminal outcomes, and duplicate events without running workers.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish acceptance from successful completion.
  • Test valid transitions, bounded polling, and duplicate event handling locally.

Treat acceptance as the beginning

A response that accepts an export job does not prove that an export exists or will succeed. The contract needs a way to observe progress and terminal outcomes, plus authorization rules for the job and result. Define whether queued, running, succeeded, failed, and cancelled are supported states and which transitions are legal. Include the meaning of a missing or expired job. The course uses a fictional export of public book metadata and never starts a worker, queue, storage service, or network request.

Validate a state machine

An explicit transition table reveals impossible regressions and missing terminal handling. A succeeded job should not silently return to running under this classroom contract. The first example checks a successful path and then verifies that a prohibited transition is absent from the table. It does not implement message delivery or durable state. In a real system, retries and duplicate messages can repeat an observation without representing a new transition, so distinguish repeated state from an illegal state change. Define monotonicity only where the actual workflow promises it.

python A fictional export transition table
allowed = {"queued": {"running", "cancelled"}, "running": {"succeeded", "failed"}, "succeeded": set(), "failed": set(), "cancelled": set()}
path = ["queued", "running", "succeeded"]
for before, after in zip(path, path[1:]):
    assert after in allowed[before]
assert "running" not in allowed["succeeded"]
print("workflow transition policy checked")
Expected output
workflow transition policy checked

The complete program checks a three-state path and one prohibited regression. The set of transitions is an example policy, not a universal queue lifecycle. No background processing or persistence is exercised.

Bound observation without arbitrary sleeping

An asynchronous test should have a deadline and meaningful terminal assertions. Sleeping for a fixed interval and assuming completion can be both slow and flaky. A polling helper needs a maximum observation budget, terminal failure handling, and diagnostic evidence when the budget expires. The local example consumes a fixed sequence rather than waiting in real time. It demonstrates that acceptance and progress do not count as success. Actual scheduling, backoff, cancellation, and service deadlines need separate verification in an approved environment; no polling traffic is generated here.

python Check success, failure, and incomplete observation
def observe(states, limit):
    for state in states[:limit]:
        if state in {"succeeded", "failed", "cancelled"}:
            return state
    return "OBSERVATION_BUDGET_EXHAUSTED"

assert observe(["queued", "running", "succeeded"], 3) == "succeeded"
assert observe(["queued", "running", "failed"], 3) == "failed"
assert observe(["queued", "running"], 2) == "OBSERVATION_BUDGET_EXHAUSTED"
print("bounded observations checked")
Expected output
bounded observations checked

The complete program makes three deterministic decisions over local arrays. The limit counts observations, not seconds. It creates no delay and does not claim a real job meets a latency or availability target.

Separate delivery from business effects

A queue may deliver a message more than once under its documented guarantees. Test that duplicate delivery does not create duplicate protected results when the consumer contract requires suppression. Define the deduplication scope and retention, and include out-of-order progress events if they are possible. A terminal event arriving twice may be harmless, while a second notification may violate the business promise. Check the durable effect independently from the count of received messages. Avoid claiming exactly-once execution merely because a local handler ignores one repeated identifier.

Record the observed sequence and remaining uncertainty

When a workflow test fails, preserve the synthetic job id, observed state sequence, relevant event identifiers, and the final protected result. Distinguish a known terminal failure from an observation timeout: the latter leaves the actual outcome uncertain. Do not automatically delete or rerun an uncertain job without the contractโ€™s reconciliation rule. Include result expiry, permission changes, and cancellation races where supported. The offline models clarify expected decisions and diagnostic categories, while real worker execution, durable delivery, and result authorization remain explicitly untested by these examples.

Common errors and fixes

A 202 response is counted as export success

Why it happens
Acceptance is confused with terminal completion.
How to fix it
Observe the documented terminal state and validate the result.
Prevent it next time
Keep acceptance and completion assertions separate.

A fixed sleep causes intermittent failures

Why it happens
The test assumes a scheduling duration.
How to fix it
Use bounded observations with explicit terminal and timeout outcomes.
Prevent it next time
Control clocks locally and document approved integration deadlines.

Duplicate delivery sends two completion notices

Why it happens
Delivery count is mistaken for unique business work.
How to fix it
Apply the reviewed deduplication and effect boundary.
Prevent it next time
Test repeated and out-of-order synthetic events.

Check your understanding

1. A polling budget expires after running. Is the job known to have failed?

Show worked solution

No. The observation is incomplete, so the actual job outcome remains uncertain. Report the last state and budget exhaustion separately from terminal failure. Use the documented reconciliation or cancellation process in an authorized environment rather than assuming a rerun is safe.

2. Design a duplicate-event case for a completed public catalog export.

Show worked solution

Deliver the same synthetic completion identifier twice to a local consumer model. Require one protected result and one notification if that is the contract, while allowing two recorded deliveries. Then state that durable deduplication and crash recovery need separate approved persistence evidence.

Key takeaways

  • Acceptance, progress, terminal failure, and incomplete observation differ.
  • A workflow test needs bounded observations and a final-state oracle.
  • Duplicate delivery and duplicate business effects are separate quantities.

Official references