Quick answer: Cache invalidation is controlled by response headers, not by a separate delete call. Cache-Control sets how long a response stays fresh and whether it can be stored, and ETag plus a conditional request handles revalidation once that window passes. Test the fresh window, the stale window with a 304, a changed ETag returning 200 with a new body, no-store never being cached, and Vary splitting the cache by the right dimension. The assertions should cover both whether the client sends a request and which conditional headers it carries, since a stale response that silently serves old data is the failure you are hunting.
What the headers mean
Cache-Control: max-age=60 marks the response fresh for 60 seconds; no-cache forces revalidation before reuse even though the response may be stored; no-store forbids storage entirely; private limits the response to a single user's cache while public allows shared caches. The server also sends an ETag, and on the next request the client echoes it in If-None-Match. If the resource is unchanged the server returns 304 with no body and the client keeps its copy; if it changed, the server returns 200 with a new body and a new ETag. Last-Modified and If-Modified-Since do the same job at one-second precision.
The test matrix
| Case | Response headers | Next request | Expected |
|---|---|---|---|
| Fresh window | max-age=60 |
None sent | Cached copy used |
| Stale, unchanged | no-cache, same ETag |
If-None-Match sent |
304, copy reused |
| Stale, changed | New ETag | If-None-Match sent |
200, new body |
| Never stored | no-store |
Full request each time | 200, no reuse |
| Split cache | Vary: Authorization |
Separate request per identity | No cross-user copy |
| Private data | private, max-age=0 |
Revalidated per user | Not shared |
Deterministic timing and revalidation
Real wall-clock waits make these tests slow and flaky, so drive the clock or set short, fixed max-age values against a mock you control. Assert the request count: inside the fresh window the client should not call the network at all, and after expiry it should send a conditional request rather than a blind full fetch. When the ETag matches, confirm the client treats 304 as a valid response and does not reset state or flash a loading state; when it differs, confirm the new body replaces the old one and the stored ETag updates. A client that ignores 304 and shows an empty view, or one that never revalidates and serves stale data forever, both fail here.
Vary and cache busting
Vary tells the cache which request headers change the response, so a response that differs per user or per encoding must list those dimensions or one user gets another's copy. Assert that two identities with the same URL do not share a stored response. For resources that change on deploy, fingerprinting the URL with a content hash is more reliable than waiting for expiry: a new file name is a cache miss by definition, while the old URL stays cached. In a desktop API workspace you can run each header combination as an ordered scenario and inspect the exact conditional request and response. Powerduck works this way, making the revalidation sequence repeatable instead of relying on browser cache behavior.
Honest limits
These tests prove the client and HTTP layer honor the headers you configure, but they do not prove how a real browser, CDN, or corporate proxy will cache the same response. A local mock cannot reproduce multi-layer caches, clock skew, purge propagation delay, or a CDN's default rules, so confirm the production path with a small set of genuine requests and check the Age header. The checks do not cover server-side cache invalidation or a purge that fails silently, and a passing run says nothing about whether the stored content is correct. Importing a schema does not deploy a backend, and a green run is not evidence about the upstream service.
Frequently asked questions
What is the difference between no-cache and no-store? no-cache allows storage but requires revalidation before reuse; no-store forbids storing the response at all.
Does a 304 return a body? No. The server sends headers only, and the client keeps using its stored body when the ETag still matches.
Why does my changed response still show old data? The max-age window has not expired, the ETag did not change, or Vary is missing. A fingerprinted URL avoids the first two.
Should I test real wall-clock expiry? Use short fixed ages or a controllable clock for determinism, then confirm one genuine end-to-end path against the real CDN.
The bottom line
Treat invalidation as explicit branches: fresh reuse, stale revalidation with 304, changed ETag with 200, no-store, and Vary-split entries, asserting the request count and conditional headers rather than only the rendered view. Confirm the real CDN behavior separately. For more on this approach, see the API testing guide hub.
Comments
Join the discussionNo comments yet. Be the first to comment.

