Adjectives are not criteria
“The export should be fast.” “The form should be robust.” “Handle errors gracefully.” Each of these passes review in a ticket and specifies nothing, because every reader supplies a different meaning and nobody finds out until the disagreement is expensive.
The repair is mechanical. Replace each adjective with an observation somebody could make.
- Fast → 95th percentile under 300 ms at 50 requests per second, on the production dataset size.
- Robust → a malformed row does not abort the import; it is skipped, counted, and reported in the summary.
- Graceful → on a provider timeout, return 503 with
Retry-After: 30, log at warning, do not retry a non-idempotent call. - Secure → a user cannot read another tenant’s records through any endpoint in this module, tested with a second tenant’s token.
Each rewrite took twenty seconds and each is now something a test can assert and a reviewer can check.
The Given / When / Then shape
One format, used consistently, does most of the work. It forces you to state the starting state, which is where the omissions live.
Given a cart with 3 items totalling ₹2,400
And a promo code SAVE10 valid until 30 September
When the customer applies SAVE10 on 29 September
Then the total becomes ₹2,160
And the discount line reads "SAVE10 −₹240"Then the negative cases, which is where the real specification lives:
Given the same cart
When the customer applies SAVE10 on 1 October
Then the code is rejected with "This code has expired"
And the total is unchanged
Given a cart with SAVE10 already applied
When the customer applies SAVE10 again
Then the discount is not applied twiceThree criteria and you have already decided expiry semantics, idempotence and the exact user-facing string. All three are decisions somebody would otherwise make silently.
The numbers matter more than the prose
The strongest criterion is a worked example with real values. ₹2,400 with SAVE10 gives ₹2,160 is checkable by a human in two seconds, encodes the rounding rule, and cannot be satisfied by code that misunderstands the requirement.
Compare with “applies a 10% discount”, which is satisfied by code that discounts before tax, after tax, on shipping too, or that rounds half-up when your finance team rounds half-even. The worked example resolves all of those without anybody having to think of them as questions.
This matters more than it used to for a specific reason. A test whose expected value was derived from the implementation asserts whatever the implementation does. A worked example in the specification comes from outside the code, which is the only thing that makes an assertion evidence.
Criteria become test names, nearly verbatim
def test_promo_applies_before_expiry(): ...
def test_promo_rejected_after_expiry(): ...
def test_promo_not_applied_twice(): ...
def test_promo_discount_line_text(): ...Run pytest --collect-only -q and you have the specification back, in a form that is executable and that will fail when somebody breaks it. That is what makes the criteria non-rotting: they are checked on every commit, and a criterion nobody checks is a wish.
The mapping should be close enough to be boring. If a criterion cannot be turned into a test name, it is usually not a criterion: it is an aspiration, and it needs another pass.
The criteria that cannot be automated
Be honest that some cannot. “The error message makes sense to a non-technical user.” “The screen is usable one-handed.” “The wording does not sound accusatory.”
These are real requirements and they need a named human and a moment in the process, not a test. Write them in a separate section headed checked by a person, with a name against each. What you must not do is either drop them because they are not automatable, or pretend a test covers them.
The definition-of-done question
One more line, and it is the one that catches the largest omissions:
What must be true, other than the code working, before this is finished?
Usually: the migration has run on production, the flag exists, the dashboard shows the new metric, the runbook mentions the new failure mode, the support team knows the new error message, the old code path is deleted.
That list is where features go to sit at 95% complete for three weeks. Writing it at the start does not make it shorter. It makes it visible, and visible work gets scheduled.
Every lesson here is open. Your progress is recorded once you pass the AI at Work exam.
Check your understanding
A ticket says “apply a 10% discount”. The implementation discounts the subtotal before tax; finance expected the discount after tax. Both readings pass every test that was written. What would have prevented this?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Larissa, your AI tutor
