How to turn requirements into test cases that hold up
By qtrl Team · Engineering
Most test cases get written from a ticket that says something like "user can upload a profile photo." That's not a requirement, it's a feature name. Turning it into tests means making a dozen decisions nobody wrote down: which formats, what size limit, what happens on a failed upload, whether the old photo survives, whether the crop is mandatory.
You can either make those decisions yourself and find out later you guessed wrong, or you can extract them properly. Here's a process for the second one, including where to hand work to an agent and where not to.
Step 1: split the requirement into testable statements
A requirement you can test has three parts: a condition, an action, and an observable outcome. If any of the three is missing, you have a feature description, not a requirement.
Take the photo upload. Broken into testable statements it becomes something like:
- When a signed-in user selects a JPEG under 5MB, the system stores it and displays it as their avatar
- When a user selects a file over 5MB, the system rejects it and shows a size error
- When a user selects an unsupported format, the system rejects it and shows a format error
- When an upload fails mid-transfer, the system keeps the previous avatar and shows a retry option
- While an upload is in progress, the system disables the upload control
Five statements from one sentence, and writing them out surfaced two questions the ticket never answered: what the size limit is, and whether a failed upload clobbers the existing photo. Take those back to whoever owns the feature. That conversation is the highest-value fifteen minutes in this whole process.
If your team is working in a spec-driven workflow, the statements may already exist in this shape. Half the job is done.
Step 2: find the cases the requirement doesn't mention
Requirements describe intended behavior. Bugs live in the space around it. Four techniques cover most of that space, and they're worth running as an explicit pass rather than relying on instinct.
Boundary analysis. Wherever there's a limit, test on it, either side of it, and at zero. A 5MB limit means testing 5MB exactly, just under, just over, and an empty file. Off-by-one errors at boundaries are the most common bug class in software and always have been.
Equivalence partitioning. Group inputs that should behave identically and test one from each group rather than all of them. Every valid image format doesn't need its own case. One valid format, one invalid format, and one file with a lying extension will do.
State transitions. What happens if the user does the thing twice? Navigates away mid-action? Comes back with a stale tab? Two uploads racing each other is a real case and it's never in the ticket.
The error path nobody specified. Storage is full. Network drops at 90%. The auth token expires during a long upload. A requirement almost never covers infrastructure failure, and users hit it anyway.
Step 3: write the case so someone else can run it
A test case that only its author can execute isn't an asset. The format that survives handoff:
| Field | What goes in it |
|---|---|
| Title | The behavior being verified, not the steps. "Upload rejects files over 5MB" |
| Preconditions | Account state, data state, feature flags. Everything true before step one |
| Steps | What the user does, in order. No assertions mixed in |
| Expected result | What is observably true afterward. Specific enough to disagree with |
| Requirement link | Which statement from step 1 this covers |
The expected result is where most test cases go soft. "Error is shown" leaves the runner to decide whether the error they got was the right one. "A message reading 'File must be under 5MB' appears below the upload control, and the previous avatar is unchanged" can actually fail.
The requirement link is the field teams skip and then wish they had. It turns "what's our coverage" from an opinion into a query.
Step 4: decide what gets automated
Not every case earns a script. The rough sort:
- Automate anything that runs every release, has a deterministic outcome, and is expensive to check by hand. Happy paths on core journeys, validation rules, regression cases for fixed bugs.
- Keep manual anything about judgment: does this layout look broken, is this error message actually helpful, does the flow make sense to a first-time user.
- Automate later the cases for features still changing shape weekly. Automating an unstable UI is how you end up maintaining tests instead of writing them.
The trap is automating in ticket order rather than value order. Risk-based prioritization answers which cases deserve the effort first.
Where an agent helps, and where it doesn't
Generating first-draft cases from a well-structured requirement is something models are good at. Give an agent the five statements from step 1 and it will produce a reasonable set of cases with boundaries included, in less time than it takes to format the table.
It falls down in two places, though. It won't notice the requirement didn't say what happens to the old photo, because it will fill that gap with a plausible assumption and write a confident case asserting it. And it won't know that uploads are the feature your team broke twice last quarter, which is the context that should change what you cover heavily.
So: agent drafts, human reviews, and the review specifically looks for assumptions the requirement never made. That's a different reading pass than checking whether the cases look sensible. They will look sensible. That's the problem.
The habit that makes this stick
Write the testable statements during refinement, before the code exists. Not because the process is purer that way, but because that's when the ambiguity is cheap to resolve. A missing acceptance criterion found in refinement costs a question. The same one found in testing costs a rewrite, and found in production it costs an incident.
Most of the value in test case design is the questions it forces, and those questions are worth more the earlier you ask them.
qtrl keeps test cases as structured records with their preconditions, steps, expected results, and links back to what they verify, so coverage is something you can look up rather than reconstruct. Cases stay readable by people and executable by agents.
Agent runs then execute them against real browsers, with each run logged against the case it was meant to check. You get the traceability of managed test cases and the throughput of automation, without maintaining two parallel systems. See how it works.
Have more questions about AI testing and QA? Check out our FAQ