Course outline

Software QA / Intermediate: integrated product evidence / Lesson 7

API contracts, errors, and repeated requests

Test more than status codes. Build evidence about request validation, response meaning, authorization boundaries, and duplicate effects using a fictional enrollment API in a controlled environment.

About 4 min readContent reviewed
On this page

Lesson goals

  • Derive contract tests from agreed API behavior.
  • Assert response and persistent side effects together.
  • Design retry and negative cases safely.

A contract includes meaning

An API contract specifies methods, paths, required fields, types, response structures, and error behavior. A schema match alone cannot establish that an enrollment belongs to the requesting learner or that the charged amount is correct. Agree semantic invariants alongside the schema: one active enrollment per learner and course, prices in integer minor units, and no private fields in public responses. Use RFC-defined HTTP semantics as a foundation while documenting application choices. For example, an application may choose a consistent validation response, but the test must reference that choice rather than assume a universal status.

Worked case: create and read

Seed a learner who is permitted to enroll and a course with an available seat. Submit the agreed valid body and expect the application’s creation response, a stable identifier, the correct learner and course, and exactly one stored enrollment. Read that identifier through the supported endpoint and compare business fields. A response containing an identifier without a persisted resource is a failure even if its JSON is valid. Keep token values out of reports and record the request body using synthetic identifiers so another tester can repeat the scenario safely.

text Example application contract, not universal status prescriptions
Case | Request condition | Expected observation
Create | valid learner/course | agreed 201; id; one resource
Read | returned id as owner | 200; matching business fields
Invalid | course_id missing | agreed validation error; no resource
Forbidden | another learner’s resource | agreed denial; no private fields

Assertions cover identity and effects as well as status and structure.

Negative responses need positive assertions

Test missing fields, wrong types, unknown identifiers, invalid transitions, and unauthorized actors with otherwise valid data. Check that rejection leaves state unchanged and returns a useful, consistent error without internal stack traces. Distinguish an unauthenticated caller from an authenticated caller lacking permission; the contract may conceal resource existence deliberately. A 500 response to malformed input is not the expected validation result. Avoid accepting any non-success code as a pass, since an unavailable service could make every negative test appear successful.

Worked case: ambiguous timeout and retry

Assume the enrollment endpoint defines an application idempotency key. Arrange a request whose write succeeds but whose response is deliberately withheld by a local test double. Retry the identical request with the same key. Expect the original enrollment identity and no second seat consumption. Reusing that key with a different course should follow the agreed conflict behavior. POST is not inherently idempotent merely because this application offers a key. Verify key scoping, retention assumptions, and persistent effects rather than asserting only that two response bodies happen to match.

Separate provider contracts from local simulations

Consumer tests express what the client needs; provider-side checks show whether the service supplies it. Version the examples and include compatibility cases for optional fields, missing fields, and changed meanings. Local stubs make timeouts reproducible, but cannot establish the real provider’s current behavior. Keep a controlled integration check where authorized and feasible, and label untested external assumptions. When reviewing results, note the contract revision, endpoint configuration, account role, and data setup. This makes a contract failure distinguishable from misconfiguration or an expired test credential.

Common errors and fixes

Asserting only HTTP 200

Why it happens
Transport success is mistaken for correct business behavior.
How to fix it
Assert identity, required fields, values, and persistent effects.
Prevent it next time
Review every API test for a semantic oracle.

Any error satisfies a negative test

Why it happens
The expected failure mode is unspecified.
How to fix it
Assert the agreed error category and unchanged state.
Prevent it next time
Keep a healthy baseline case in the same controlled environment.

Retry testing ignores duplicate side effects

Why it happens
Only response equality is examined.
How to fix it
Inspect resource count, seat count, and key behavior.
Prevent it next time
Include an ambiguous-response scenario in the contract.

Check your understanding

1. A malformed request returns 500. The negative test passes because status is not 200. Repair the oracle.

Show worked solution

Assert the agreed validation status and machine-readable error code, and verify that no enrollment was created. A 500 should fail this test because it indicates an unexpected server error, even though the request did not succeed. Include a valid control request to expose general service unavailability.

2. What distinguishes a safe repeated create from two matching responses?

Show worked solution

The persisted effect must occur once: one enrollment identity, one seat consumed, and no duplicate payment request. Matching responses could hide two writes. Test the agreed idempotency key behavior, including conflict on a changed body and isolation between different actors.

Key takeaways

  • Status, schema, semantics, and side effects provide different evidence.
  • Document application retry rules instead of assuming them from an HTTP method.

Official references