From Requirements to Test Cases: How I Turn Features into Testable Scenarios
A feature request describes a destination. A test case describes how we will know we arrived. Between them are questions that are easy to skip.
The Lumé House examples below are reconstructed from local QA records and rewritten for publication. They are illustrative, not verbatim requirements or fresh results. Customer data and unverified execution claims are excluded.
Why requirements need questions
"Customers should be able to choose delivery or pickup at checkout" sounds complete. It is not.
Which fields appear? What makes an address usable? Is delivery available everywhere? What happens on a mode switch? Which rule belongs to the browser, client or server?
"Required" can mean rendered, non-empty, meaningful after trimming, or accepted by the server. Four claims, four tests.
A test case needs an oracle: the requirement, a business rule, a data constraint or an approved design. "It looked right" cannot. In my QA work, I compare behaviour with requirements and record the expected result, the actual result and the difference.
From feature request to acceptance criteria
I turn a request into acceptance criteria before writing cases. Each names an actor, condition, action, observable outcome, source and version.
For the checkout example, the criteria might include:
- Given delivery, when the customer submits without a street address, then submission is blocked, focus moves to the first invalid field, and an error is associated.
- Given pickup, when the customer submits, then delivery-only fields are not required and no delivery address is sent.
- Given a delivery zone, when the total is calculated, then the server-selected fee is used, not a browser-supplied one.
If the original is unavailable, I label the criterion as reconstructed and say what it was reconstructed from. That tells the next person how much weight it can bear, and stops a test artifact becoming the requirement.
I separate business rules (what the service offers), UI rules (what the customer sees) and data rules (what the system stores or rejects). One end-to-end test can touch all three without telling you which failed.
Delivery versus pickup: a worked example
The wording below is an illustrative reconstruction, not the original stakeholder requirement:
Customers may choose delivery or pickup. Delivery requires contact details and a valid address; pickup does not require an address. The total must reflect the delivery fee for the selected zone.
The field inventory:
| Field group | Delivery | Pickup |
|---|---|---|
| Contact: name, phone, email | Required | Required |
| Delivery: street, area, city, zone, instructions | Required, except instructions | Not required |
| Payment: provider, mobile-money number | Required | Required |
The inventory answers which fields appear, not the useful questions. Is the city free text? Is delivery restricted? Are delivery fields hidden or disabled on pickup? Are values cleared on a mode change? Is the fee calculated client-side or server-side? Does a hidden field still submit?
A 19 October 2026 review inspected a branch snapshot. An automated guest-checkout check against a local mock found native validation blocking blank required fields and focusing the first invalid control. Whitespace-only values passed native validation for some text fields, and the server-side validator rejected them generically.
Those findings describe one branch, not the current build. A finding becomes a defect only when reproduced against a named build with evidence.
The lesson: "delivery details are required" produced three questions. Is the field present? Is the value meaningful? Is the feedback useful?
Positive, negative, boundary and conditional cases
A positive case uses valid input and checks the intended outcome. A delivery order with a complete synthetic address should reach the server with the expected fields; pickup should omit delivery-only values. The oracle is the accepted order state and payload, not a clickable button.
A negative case uses input that should be rejected. Each required field gets a missing-value case; malformed phone and email values get their own. An unsupported city, missing zone, empty Cart and unauthenticated session fail differently.
A boundary case sits at a rule's edge. Empty differs from whitespace-only; a city with trailing spaces differs from an unsupported city name. Cart quantities of 1, 99 and 100 test the range and first rejection.
A conditional case changes the state that determines which rules apply. Switching to pickup after an invalid address should not leave a stale error blocking pickup; switching back restores the delivery requirement.
| Type | Question | Example | Oracle |
|---|---|---|---|
| Positive | Valid delivery produces an order? | Complete address, selected zone | Order accepted with expected fields |
| Negative | Missing zone rejected? | Zone left unselected | Blocked, zone-specific message |
| Boundary | Whitespace treated as empty? | Street contains only spaces | Field-specific rejection, no order |
| Conditional | Pickup ignores delivery errors? | Invalid street, then pickup | Pickup submits; no delivery values |
A good suite covers partitions, not every possible string. The aim is to make each rule visible, not prove every combination.
A traceability table with expected results
A traceability table links each requirement to its cases, layer and evidence. It exposes gaps and stops a passing check at one layer being described as proof at another.
| ID | Requirement | Action | Expected result | Layer | Status |
|---|---|---|---|---|---|
| T01 | Delivery needs a street address | Submit street blank | Submission is blocked, the first invalid field receives focus, and a relevant error message is displayed. | Browser | Designed; historical observation |
| T02 | Address must be meaningful | Submit spaces only | Field-specific rejection; no order | Browser + backend | Historical divergence; not retested |
| T03 | Delivery is limited to one city | Enter unsupported city | City message; no order | Browser + backend | Historical gap; not retested |
| T04 | Delivery needs a zone | Submit with no zone | Blocked; a zone message appears | Browser | Designed; field association was a historical gap |
| T05 | Pickup skips delivery fields | Choose pickup, submit valid details | Delivery values absent or null | Harness + payload + backend | Harness observed omission; integrated blocked |
| T06 | Mode switch clears conditions | Invalid delivery data, then pickup | Pickup not blocked by hidden errors | Browser | Designed; not run |
| T07 | Server owns the delivery fee | Tamper with browser fee or zone label | Server fee used; tampering ignored | Backend | Source-inspected; live not verified |
"Expected result" matters most. "No error" is not an oracle; it describes an absent signal. "No order is created, the response identifies the field, and the record has no delivery address" is an oracle because it can be checked independently.
No row says PASS. Some have historical observations, one is source-inspected, one blocked, several designed but not run. The table claims only that we know what evidence would settle each question.
What the Cart conflict example taught me about test layers
Let's consider another example: a shopping cart conflict. This is an illustrative scenario reconstructed to demonstrate how a requirement becomes a test case, rather than a direct quotation from a stakeholder.
If the server rejects a Cart change because the quantity changed elsewhere, the client must not silently retry. It must ask the customer to refresh and review, then send a new expected quantity.
Each change carries an operation id. A change or removal on an existing line also carries the expected quantity; an add sends none.
The mobile repository contains an offline contract test for the stale-quantity path, using a stand-in repository. It loads quantity 2, tries to set 3, and the stand-in rejects it as CART_CHANGED. It asserts an error state, a message about another session, and no automatic retry; a second identical change is refused. It refreshes to 4, submits 5, and checks expected quantities 2 and 4, with different ids.
That is a meaningful client-contract result: it does not loop on a conflict it can treat as final. It refreshes and builds the next request from the reviewed state.
It is not proof that the server rejects stale writes. Every repository is mocked; the real database rule, session and network are outside the test.
| Question | Browser observation | Mocked client contract | Live backend |
|---|---|---|---|
| Customer-visible behaviour? | Yes | Partly | No |
| Client conflict handling? | Not reliably | Yes | No |
| Server rejects stale quantity? | No | No | Yes |
| Ownership isolation? | No | No | Yes |
A mocked test is not lesser; it has a narrower claim. It can be precise about the client state machine while saying nothing about the server. A browser observation can show the message while saying nothing about the database. A live backend check can prove the rule but not what the customer understands.
The manual equivalent is a two-session scenario: load the Cart in both, change one, then try from the stale state in the other. It is designed, not executed. Until it runs against a named environment, the mock is the only evidence for client behaviour.
Designed is not executed
A test case is a plan. A test result is a claim about an execution. The difference needs to survive into the report.
| Status | Meaning |
|---|---|
| Designed / planned | Case exists; no execution claimed |
| Not run | Not executed in this cycle |
| Blocked | Precondition prevented execution; not a failure or pass |
| Fail | Observed differs from expected |
| Pass | Expected observed on a named build, with evidence |
The integrated manual QA pack is an example. Its delivery and pickup cases were BLOCKED because the integrated environment and approved test writes were unavailable. No browser case was marked passed. That is a result about the environment, not proof that checkout was broken. A blocked prerequisite is neither a failure nor a pass.
The same discipline applies to automation. A synthetic-session DOM harness is not a real browser; an automated check against a local mock is not a manual integrated session. Each produces evidence for a narrower sentence than "checkout works".
A status has a scope. "Pass in an automated local-browser check against a mock on 19 October 2026" differs from "pass in the current production build". Without build, environment or date, it is not reproducible.
Defects, retesting and evidence
A defect record is a test case with a disagreement attached. It should contain the requirement or case id, environment and build, preconditions, steps, expected and actual results, evidence and severity. The actual result describes what happened, not an assumed cause.
The whitespace finding is an example. Illustratively, its record would read: the expected result was a field-specific rejection for a street address of only spaces; native validation accepted the spaces-only value, the request reached the server-side validator, and the rejection came back as a generic message without field association. The record recommended a client-side check and better feedback; it did not claim a fix.
A retest is not "the code looks different now". It is the same case, on the corrected build, with the same preconditions, producing a new result and evidence. The original failure stays. If the case passes, the history shows what changed; if not, the defect remains open.
A code change can create regressions nearby. After a validation fix, I would rerun the positive delivery, pickup, whitespace boundary and error-association cases. A unit test at the client layer does not replace a browser retest, and a browser retest does not replace a backend check.
Evidence should be proportional and safe: a date, environment, build reference, command or short sanitized capture is enough. Customer names, phone numbers, addresses, order references, tokens and raw payloads do not belong in a public write-up.
Test counts need care. A suite total is a snapshot, not coverage of a requirement. If I cite a count, I date it and say what it covered.
Closing: what good test cases help us understand
Good test cases do not prove that a feature is perfect. They make its assumptions visible.
The checkout example showed that a conditional requirement is a set of questions about presence, meaning, feedback and server authority. The Cart example showed that client tests, browser observations and live backend checks answer different questions. Neither needed a bug story; the work was in the questions.
When I turn a feature into test cases, I ask five things:
- Who is the actor, and what state are they in?
- What condition changes the expected behaviour?
- What observable outcome would make the requirement false?
- Which layer can actually prove that outcome?
- What is the execution status, and where is the evidence?
A requirement becomes testable when someone can disagree with it precisely. That is the standard I aim for, even when the answer is "not run yet".
Top comments (0)