Writing Edge Cases That QA Can Actually Test
Here's how to stop edge cases from becoming production bugs: write them in the spec before implementation starts, in a format QA can turn into test cases without asking a single clarifying question. The edge cases that cause real incidents aren't exotic — they're the double-submit, the empty string in a field everyone assumed was populated, the permission check that passes because the order of operations was never specified. None of these are hard to anticipate. They're just easy to skip when the spec doesn't force you to name them.
The edge cases nobody thinks to write down
Teams usually document the obvious ones: empty input, null value, 404. Those are fine. The cases that cause real incidents are the harder-to-anticipate ones — the situations that weren't exotic, just unconsidered.
| Category | What to Check | Example |
|---|---|---|
| Null / Empty | Missing fields, empty strings, zero-length arrays | User profile with email = "" |
| Boundary | Limits, thresholds, off-by-one | 99 vs 100 items in cart |
| Duplicate | Idempotency, double-submit, re-import | Same order submitted twice in 3 seconds |
| Concurrency | Simultaneous edits, race conditions | Two users edit same record |
| Permission | Role boundaries, visibility rules | Viewer tries to access admin endpoint |
| Retry / Timeout | Network failures, partial completion | Payment timeout after charge submitted |
| State Machine | Invalid transitions, out-of-order events | Cancelling an already-shipped order |
The goal of writing edge cases in a spec isn't exhaustive coverage. It's to document every scenario where the system's behavior is non-obvious and where QA would need to ask a clarifying question before they could write a test. If QA can build the test independently, the edge case is done.
Null, empty, and malformed input
The most basic category. For any field in a form, API payload, or database record, the spec should state what happens when the field is null, empty string, or contains unexpected types. These should not be left to engineering judgment during implementation.
Edge cases — User profile update: - email = null → reject with 422, message: "email is required" - email = "" → reject with 422, message: "email is required" - email = "not-an-email"→ reject with 422, message: "email must be a valid address" - email = valid, 255 chars → accept - email = valid, 256 chars → reject with 422, message: "email exceeds max length"
QA should be able to copy these directly into a test plan. If they have to guess what "invalid email" should return, the spec has not done its job.
Boundary values
Boundary value analysis is one of the most reliable ways to find bugs, and it costs almost nothing to specify. For any numeric, date, or length-bounded field, write the cases at exactly the boundary, one below, and one above.
- Quantity field with max 99: test 98, 99, 100, and 0.
- Date range: test start = end, start = today, start after end.
- Pagination offset: test 0, 1, last valid page, last page + 1.
- String length: test exactly at max, one over max, empty string.
Engineers writing code will naturally test a round number — 50 items, 10 records. Nobody naturally tests 99 vs 100. That's why it has to be in the spec.
Concurrency and double-submit
Most product specs assume a single user doing one thing at a time. Production is not that. Write the concurrent cases explicitly so engineering knows what behavior is required and QA knows what to trigger.
Edge cases — Order submission:
- User clicks "Place Order" twice within 500ms
Expected: second request is rejected with 409 Conflict
Response body: { "error": "duplicate_request", "orderId": "existing-id" }
- Two browser tabs submit the same cart simultaneously
Expected: one succeeds, one returns 409
Idempotency key: client must send X-Idempotency-Key header
If key absent: treat each request as independent (document this explicitly)
The spec does not need to specify how idempotency is implemented. It needs to specify what behavior QA should be able to verify.
Permission boundaries
Permission edge cases are frequently left out of specs because "access control is handled by the auth layer." That is not sufficient. The spec should state the behavior at specific permission boundaries, not assume a reader knows the permission model.
- Viewer tries to perform a write operation → 403, not 404 (do not leak resource existence).
- User loses role mid-session → next request fails with 403 and redirects to re-auth.
- Admin operates on behalf of a user in a different tenant → blocked, returns 403 with code "cross-tenant-denied".
- API key with read scope tries to POST → 403, not 422.
Retry and timeout paths
The system will time out. The upstream dependency will be slow. A payment gateway will return a 202 and never send the webhook. These paths need to be documented explicitly or engineering will each solve them differently.
Edge cases — Payment processing:
- Stripe returns 200, but our webhook times out
Expected: job retries webhook delivery up to 3 times with exponential backoff
After 3 failures: order moves to "payment_pending_review" state, alert fires
- User navigates away before confirmation screen loads
Expected: payment intent is not cancelled; order is persisted if charge succeeded
QA verification: simulate slow webhook, confirm order persists in DB
State machine violations
Any feature with a multi-step workflow has state transition rules. The spec should document which transitions are invalid and what happens when someone tries them — either via UI, API, or by replaying an old event.
Edge cases — Order state machine: Invalid transitions and expected behavior: - DELIVERED → CANCELLED: reject with 422, "order already delivered" - PENDING → SHIPPED (skipping CONFIRMED): reject with 422, "invalid state transition" - Replaying CONFIRM event on already-CONFIRMED order: idempotent, return 200 with current state
How to format edge cases so QA doesn't ask questions
The format matters as much as the content. Edge cases that are written as prose require QA to interpret them. Edge cases that follow a Given/When/Then or table format can be turned into test cases directly.
A simple rule: for every edge case in the spec, QA should be able to write the test name, the inputs, the trigger action, and the expected output without going back to the author. If any of those four things require a conversation, the edge case description is incomplete.
The minimum viable edge case checklist
For any feature that modifies data, involves user input, or calls an external service, run through this checklist before calling the spec ready:
- Null and empty inputs for every required field.
- Boundary values for every numeric, date, or length-bounded field.
- Double-submit or concurrent modification of the same record.
- Permission boundary: lowest-privilege user attempting the action.
- Upstream dependency timeout or error response.
- Invalid state transition if the feature involves workflow states.
- Retry of a previously successful operation (idempotency).
Not every item applies to every feature. But reviewing each one forces a conscious decision. The ones you deliberately skip are at least documented omissions — not forgotten cases.
Keep reading
Editorial note
This article covers Writing Edge Cases That QA Can Actually Test for software delivery teams. Examples are illustrative engineering scenarios, not legal, tax, or investment advice.
- Author details: Daniel Marsh
- Editorial policy: How we review and update articles
- Corrections: Contact the editor