Quick answer: A mock that only returns instant success cannot test how a client behaves when an API is slow, times out, or fails intermittently — and those paths are where real resiliency lives. Latency and fault injection are deliberate mock features that model the bad conditions a client must survive: added delay, connection errors, 500s, and rate limits. The key engineering choice is to keep those scenarios reproducible and tied to the contract rather than hand-tuned in a GUI, so a slow-path test is deterministic and the fault response still matches the documented schema.
Teams build happy-path mocks first and assume fault handling is covered by the client library. It is not. A retry, a timeout, or a degraded UI has to be exercised against a target that actually produces the condition, and the only safe place to do that is a mock. Pointing such tests at a staging server means waiting for a real failure or manually breaking an environment, which is neither repeatable nor isolated. Injecting the fault into the mock puts the bad condition under test control and keeps it available on every run.
The conditions worth modeling
Four fault classes cover the resiliency behavior most clients need. Each maps to a specific client decision, which is what the test should assert.
| Injected condition | Client behavior to verify | What a green test proves |
|---|---|---|
| Fixed latency above timeout | Timeout fires, no hang | Bounded wait and error path |
| Connection reset / 502 | Retry with backoff | Transient failure recovery |
| 500 with problem body | Degraded, safe message | No raw error to users |
| 429 with Retry-After | Back off per header | Rate-limit compliance |
GUI-configured faults versus contract-driven scenarios
Desktop mock tools let you add delay and error responses per route, which is fast for exploration but fragile for automated tests: the configuration lives in a tool-specific file, values are hand-entered, and the fault body may not match the API schema. A contract-driven scenario derives the fault from the OpenAPI document, where the error status and its schema are already declared, and expresses the delay in a portable config the build can run.
Because the fault references a documented response, a contract check can verify the injected 500 has the same shape the real API returns, rather than a fabricated object the client cannot parse. The latency is a fixed value, so the slow-path test is deterministic rather than dependent on a manual toggle.
What to assert, not just produce
Injecting the fault is only half the work; the test must pin the client's observable response. A slow request should resolve to a defined timeout state within a bounded wall-clock window, a transient 500 should trigger a bounded number of retries with backoff, and a permanent error should render a safe message without leaking internals. Asserting the retry count matters because an unbounded retry under a 500 is itself a failure mode that a fault test is perfectly placed to catch.
it("times out and surfaces a safe state, never hangs", async () => { mockScenario("upstream-timeout"); const start = Date.now(); const result = await client.getOrder("ord_1"); const elapsed = Date.now() - start; expect(result.timedOut).toBe(true); expect(elapsed).toBeLessThan(3500); // bounded, did not wait forever });
Keep scenarios aligned with the spec
A local-first OpenAPI workflow keeps fault scenarios next to the contract and generates the mock from both, so the target that produces slow and failed responses is recreated identically in CI and on every machine. As the API evolves, linting and contract validation keep the injected faults honest, removing a class of flaky GUI-managed mocks. The goal is not to make failures rare in testing — it is to make every bad condition the client might face in production a routine, repeatable test.
Frequently asked questions
Why inject latency when I can just lower the client timeout?
Lowering the timeout changes the production setting to make a test pass, which is backwards. Injecting delay keeps the real timeout and proves the client behaves correctly when the genuine threshold is crossed.
Are fault tests inherently flaky because they involve timing?
They are stable when the injected delay is a fixed value and you assert bounded outcomes rather than exact wall-clock durations. Avoid asserting precise elapsed milliseconds; assert that the timeout state is reached within a defined upper bound.
Should I test every error status with a separate scenario?
Cover the conditions that drive distinct client behavior: timeout, transient retry, permanent error, and rate limit. Statuses that the client handles identically can share a parameterized scenario.
What if the mock's fault body drifts from the real API?
Use the error schema declared in the OpenAPI document and validate the injected response against it, so a fault in testing matches the shape of a real failure. That is the main reason to keep fault scenarios tied to the contract rather than free-form in a GUI.
This is part of the Powerduck vs Mockoon comparison — start there for the full decision around mock servers and how they are configured.
Comments
Join the discussionNo comments yet. Be the first to comment.

