ap.Abhishek Patel

Search

Search notes, essays, projects, and the library. Use arrow keys to choose and Enter to open.

← Notes

Why Features End Up With No Test Cases, and How to Make That Structurally Impossible

Created · Updated · 9 min read

Why this matters: this is the question I started from. We have a large product, we keep shipping, and the honest answer to “is this tested” is usually “someone clicked through it once”. This note explains how that happened and how to make it structurally impossible from here on.

Nobody decided to skip testing

That is the part worth understanding. There was never a meeting where we chose to ship untested features. What happened is that testing sat outside the definition of finished, so under pressure it was the thing that quietly did not happen, two hundred separate times.

The mechanism, every time:

flowchart TD
    A["Requirement is vague"]
    B["No acceptance criteria written"]
    C["Nothing concrete to write test cases from"]
    D["Testing becomes "click around and see if it looks right""]
    E["Sprint is tight, the clicking gets shorter"]
    F["Feature ships"]
    G["Six months later: what is this supposed to do? nobody knows"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G

Every arrow in that chain is fixable, but the first one causes all the rest. This is the same root cause described in Why We Miss Deadlines, seen from the quality side.

The chain that fixes it

flowchart LR
    A["Story"]
    B["Acceptance Criteria"]
    C["Test Cases"]
    D["Automated regression"]
    E["Living documentation"]

    A --> B
    B --> C
    C --> D
    D --> E

Read that left to right and notice that the expensive thinking happens in box two, during refinement, before any code exists. By the time you reach test cases, you are not inventing anything, you are transcribing decisions you already made.

That is the entire trick. Test cases feel expensive when you write them from a finished screen, reverse engineering intent. They are nearly free when they fall out of Acceptance Criteria that were written during refinement.

A Story with six acceptance criteria generates at least six test cases, and they existed in draft form before the first line of code.

Make it impossible to skip

Two structural changes, and the point of both is that they remove the option rather than relying on discipline.

One: criteria are required to enter the sprint. No acceptance criteria, no sprint. That is Definition of Ready.

Two: test cases are required to leave the sprint. “Test cases written and linked to the Story” goes into Definition of Done. A Story without them is not finished, regardless of what the code does.

Once both are in place, a tight sprint can no longer produce untested features. It produces fewer features, which is visible, honest, and a decision a Product Owner can actually make. You have converted an invisible quality trade into a visible scope trade, and visible trades are the only kind you can manage.

What a test case actually looks like

Not “test the invoice screen”. That is a wish. A test case has a starting state, an action, and an expected result specific enough that two different people would agree on pass or fail.

TC-INV-014   Invoice cannot be saved when stock would go negative

Precondition: Item SKU-100 has 5 units in stock, negative stock not allowed

Steps:        1. Create an invoice for customer A

              2. Add line item SKU-100, quantity 6

              3. Click Save

Expected:     Warning "Insufficient stock for SKU-100 (available: 5)" is shown.

              The invoice is not saved. Stock remains 5.

Traces to:    Story #1423, acceptance criterion 1

That last line matters more than it looks. Traceability is what lets you answer, at any time: which acceptance criterion does this test prove, and which Story introduced this behaviour. Without it, in a year you have a pile of test cases and no idea which are still relevant. Azure DevOps Test Plans link test cases directly to work items, which is covered in Azure DevOps Setup.

Where tests should live

Not everything should be a manual test case, and not everything should be an end to end automated test. The usual shape, and the reason for it:

Unit tests cover the logic: tax calculation, stock arithmetic, date handling, validation rules. Fast, cheap, and they should be the majority. Anything involving money, rates or rounding belongs here and belongs here thoroughly, because these are the bugs that are silently wrong rather than loudly broken.

Integration tests cover the pieces working together: the API endpoint plus the database plus the permission check. Slower, fewer, and this is where most real bugs actually live, because most real bugs are in the seams between components rather than inside them.

End to end tests cover the critical user journeys through the real UI: log in, create an invoice, receive a payment. Slow and brittle, so keep the set small and only for journeys where failure is genuinely serious. Ten reliable end to end tests beat two hundred flaky ones, and a flaky suite is worse than no suite because people learn to ignore red.

Manual and exploratory testing covers what automation cannot: does this feel right, is the error message comprehensible, what happens if I do something strange. Do not try to automate exploration. A skilled tester poking at a new feature for thirty minutes finds things no written case would have caught, and those findings should then become written cases.

The edge case checklist

The complaint “nobody knows the corner cases” has a boring practical fix: use a checklist, because corner cases are surprisingly repetitive. Run any new Story through these questions during refinement:

Data boundaries. Empty, one, many, maximum. Zero, negative, very large numbers. Long strings, unicode, emoji, quotes and apostrophes in names.

State. What if it is already done, already deleted, already cancelled, in draft, locked, or being edited by someone else at the same time?

Time. Timezones. Month end and year end. Backdated entries. Leap years. Financial year boundaries. Records created before a feature existed.

Permissions. What does a user without rights see? Can they reach it by URL directly? What about a user from another organisation, or a customer’s data leaking across a tenant boundary?

Money, if you touch money. Rounding, currency conversion, tax edge cases, refunds, partial payments, reversals. Money bugs do not crash, they just quietly produce the wrong number, and that is far worse.

Failure. Third party API times out, returns an error, returns success but with garbage, or returns success twice. Network drops halfway. User double clicks Submit. Browser back button after a submit.

Volume. Works with 10 rows, does it work with 100,000? Does the screen still load, does the export still complete?

Migration. What happens to data that existed before this change? This is the one that is forgotten most often and hurts the most, because it only shows up in production where the old data lives.

Fifteen minutes with this list during refinement will find cases that would otherwise arrive as production bugs, and each one you find becomes an acceptance criterion, which becomes a test case. This is the practical answer to “why does nobody know the corner cases”: nobody was systematically asking.

Catching up on features that already have no tests

You cannot retrofit test cases for everything at once, and trying will fail. Prioritise by risk instead, which is the standard risk based testing approach:

risk = how likely is this to break  ×  how bad is it if it does

Sort every existing feature into four buckets:

  • High risk, changes often: automate first. This is where your regression suite earns its money.
  • High risk, stable: write the test cases, automate when convenient. Payments, permissions, ledger postings and anything involving customer money belong here at minimum.
  • Low risk, changes often: test cases written, automate if cheap.
  • Low risk, stable: document the expected behaviour and move on. Honestly, you may never automate these, and that is a legitimate decision as long as it is a decision.

Then give it a fixed budget: one feature per sprint gets its behaviour written down and its critical path automated. Not a testing project, not a testing sprint, just a permanent small slice. Twenty sprints later you have covered twenty features, which is roughly your whole product, and nobody ever had to ask for a three month quality initiative that would never be approved.

Pick the first ones by looking at where bugs actually come from. Your bug history is a free map of where the risk is, and it is almost always concentrated in three or four areas.

The regression suite is the real asset

A test case that runs once is a checklist item. A test case that runs on every build is an asset that keeps paying, because it converts “I hope we did not break anything” into a fact you can read in two minutes.

That is what actually buys back development speed as the product grows. Without it, every release needs days of manual regression, so releases get bigger and rarer, so each one is riskier, so you need even more manual regression. With it, you release small changes often, and the DORA research is fairly clear that this is the pattern that correlates with both speed and stability. They are not a trade off.

Writing the behaviour down is half the value

Even when a test is not automated, the written case has value that outlives the sprint, because it is the only honest record of what the feature is supposed to do.

Code tells you what it does, including the bugs. A written test case tells you what it was supposed to do. That difference is the whole reason a new engineer can join and be productive, and it is the subject of Feature Inventory and Ownership.

This becomes much more important when AI is generating most of the code, because AI can produce a working implementation from a vague prompt without anybody ever articulating the expected behaviour. The code exists, and the specification never did. What that changes is in Building with AI.

A shorter version

  • Missing test cases are a symptom of missing acceptance criteria, not of lazy testers.
  • Write criteria at refinement, in Acceptance Criteria, and the test cases are nearly free.
  • Put test cases in Definition of Done so they cannot be quietly skipped.
  • Use the edge case checklist during refinement, because corner cases repeat.
  • Attack existing untested features by risk, one per sprint, forever.
  • Automate the critical paths so that speed later is possible at all.

This connects to

Why Startups Keep Missing Deadlines: The Real Cause Behind Sprint Delays, Missing Test Cases and Forgotten Features

Sit in any startup review meeting and the same questions come back every month:

Note

How to Write Acceptance Criteria: Defining Expected Behaviour Before Development Starts

Acceptance criteria are the specific, checkable conditions a single [[How to Write a User Story That Fits in One Sprint|User Story]] must satisfy to be accepted. They belong to that one Story and nothing else.

Note

Definition of Ready: The Checklist That Decides Whether a Story Can Enter a Sprint

The Definition of Ready is a short checklist a Story must pass before it is allowed into a sprint.

Note

Definition of Done: How to Stop Calling Work Finished When Testing Is Still Pending

The Definition of Done is a single shared checklist that applies to all work, describing what must be true before anything can be called complete.

Note

How to Set Up Azure DevOps for Agile Delivery: Work Items, Fields, Tags, Queries and Test Plans

A warning first: the tool does not create the discipline. A perfect Azure DevOps configuration with no [[How to Write Acceptance Criteria|Acceptance Criteria]] and no [[Definition of Done, The Gate Out of a Sprint|Definition of Done]] produces a beautifully organised record o

Note

Why Nobody Remembers How a Feature Works: Documenting Behaviour, Edge Cases and Ownership

Right after you build a feature, you know everything about it. Every edge case, every reason you chose one approach over another, every weird case the stakeholder mentioned once.

Note

Building Software With AI: Why Faster Code Makes Specification and Review the Real Bottleneck

AI did not create our planning problems. It removed the thing that was accidentally hiding them.

Note

Referenced by

Epic vs Feature vs User Story vs Task: How to Break Work Down and Decide Which Is Which

Four levels, and each one answers a different question:

Note

Agile and Azure DevOps Terminology Cheat Sheet: Every Term Explained in One Line

Acceptance criteria are per Story and change constantly. Definition of Done applies to everything and changes rarely. Definition of Ready is a team practice, not an official Scrum artifact, while Definition of Done is.

Note

Product Delivery Playbook: Planning, Estimating and Maintaining Software as It Grows

These are my notes on why software teams keep missing deadlines, keep losing track of what their own product does, and keep ending up with a hundred features and almost no test cases. I wrote them for myself first, after watching the same failure repeat sprint after sprint and re

Note

Ready to Use Templates for Epics, Features, Stories, Bugs, Test Cases and Sprint Goals

Written as a business objective, per [[Epic vs Feature vs User Story vs Task|Work Breakdown Hierarchy]].

Note

How to Roll Out an Agile Delivery Process in 90 Days, One Change at a Time

One change at a time. A team can absorb roughly one new habit per two weeks. Introduce six at once and you get six half followed rituals and a team that concludes "process does not work here", which then blocks the next attempt for a year.

Note

Scope Creep in Sprints: How to Classify Stakeholder Feedback Instead of Absorbing It

A Story was agreed: export invoice as PDF, 5 points, acceptance criteria written.

Note

SLI, SLO and Error Budgets: Measuring Whether Your Product Is Actually Healthy in Production

A feature can pass every test case and still be failing customers, because tests run in a controlled environment on clean data with one user at a time.

Note

How to Write a User Story That Fits in One Sprint: INVEST, Splitting Patterns and Examples

The most useful thing I ever read about User Stories is Ron Jeffries' idea of the three Cs: the Card (the short sentence), the Conversation (the discussion it triggers), and the Confirmation (the acceptance criteria you agree on). The written sentence is not a specifi

Note