DEV Community

Ifeanyi Ejindu
Ifeanyi Ejindu

Posted on Originally published at qarunbook.com AI-assisted

How to write test cases someone else can run

Most test cases are written by the person who built the feature, in a hurry, for themselves. They read fine to the author and fall apart in anyone else's hands: a step that assumes you know where the button is, an expected result that says "works correctly", a check that only passes if you ran the one before it.

This post is about writing them so somebody else can run them and reach the same answer you would. That matters more now than it used to, because "somebody else" is increasingly a teammate on a different phone, a client doing acceptance testing, or an AI assistant working through your plan.

What a test case is (and what it isn't)

A test case checks one behaviour of the app. It says what has to be true before you start, the steps to take, and the result you should see at the end. If the result matches, it passes. If it doesn't, you have found a bug, and the test case is already most of the bug report.

Two terms get mixed up with it:

  • A test scenario is broader: "a customer resets their password". One scenario usually needs several test cases: the reset that works, the link used twice, the email with no account behind it.
  • A checklist item is shorter: "password reset works". Fine as a reminder for the person who wrote it. Not something a new tester can run, because it doesn't say what "works" looks like.

A test plan is the whole collection: every test case for the app, grouped into sections, with a column for each platform you ship on.

The six fields you actually need

Field Why it's there
ID A short, stable handle like PAY-02. People quote it in messages and bug reports, so it never changes once written.
Journey What is being checked, in the customer's words: "A declined card is handled", not "Validate payment error state".
Preconditions The state you must be in first, including test data: signed in or out, a saved card, aeroplane mode on.
Steps Numbered, one action each, with buttons named as they appear on screen.
Expected result What you should see, concretely enough to decide pass or fail without asking anyone.
Platforms One column per platform (web, Android, iOS). Write N/A where the feature doesn't exist.

Templates often add actual result, status, tester and date. Those belong to a run, not to the case, and they change on every build. Priority is better handled by section order (below).

Writing one, step by step

  1. Start from what the user is trying to do. Read the feature or the acceptance criteria and write down the outcome a person wants: "pay for what is in my basket". That's your scenario.
  2. List the cases before writing any of them. One line for the path that should work, then one for each way it can be refused or go wrong: wrong input, missing permission, no network, an expired session, the button pressed twice. The second list is where bugs hide.
  3. Look at the edges of every input. If a password needs at least 8 characters, test 7 and 8, not 3 and 20. And when many inputs should behave the same way, one example from each group is enough.
  4. Write the preconditions. Everything the tester needs before step one. If a case needs a declining card or a used reset link, say so here.
  5. Write the steps. One action per step, in order, using on-screen names.
  6. Write the expected result. What's on screen, what's kept, what's cleared, and what did not happen.
  7. Mark the platforms. Blank if it needs testing there, N/A if the feature doesn't exist there.
  8. Have someone else run it once. If they ask you a question, the answer belongs in the test case.

Expected results are where test cases go wrong

"The error is handled" passes as soon as any error appears. Compare:

Weak: An error is shown for a declined card.

Strong: A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders.

The strong version catches three bugs the weak one lets through: a vague message, a basket that empties itself, and an order created for a payment that failed. Put the judgement in the test case, not in the tester's head.

Five worked examples

These are from a shop app tested on web, Android and iOS. Each covers a journey nearly every app has.

SIGN-01: Sign up with an email address

  • Preconditions: Signed out. No account exists for the email you will use.
  • Steps: 1. Open the app. 2. Tap Create account. 3. Enter a name, a new email address and a password of at least 8 characters. 4. Tap Create account.
  • Expected: The home screen opens with the name you entered at the top. A verification email arrives within a minute.

The preconditions matter: an email that already has an account makes this a different case. The expected result checks two things a quick look misses, that the name was saved and that the email actually arrives.

RESET-01: Reset a forgotten password

  • Preconditions: Signed out. An existing account whose inbox you can open.
  • Steps: 1. Tap Sign in, then Forgot password. 2. Enter the account's email and submit. 3. Open the link in the reset email. 4. Set a new password. 5. Sign in with the new password.
  • Expected: The new password signs you in. Signing in with the old password fails with the usual wrong-password message.

The second half of the expected result is what makes this a real test. A reset that sets the new password but leaves the old one working looks fine from the success screen.

PAY-02: A declined card is handled

  • Preconditions: Signed in. A test card that will decline. One item in the basket.
  • Steps: 1. Open the basket. 2. Tap Checkout. 3. Enter the declining card. 4. Tap Pay.
  • Expected: A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders.

Most payment providers publish test card numbers that decline on purpose. Name the one you use in the preconditions.

NET-02: The connection drops during payment

  • Preconditions: Signed in. A saved card. One item in the basket.
  • Steps: 1. Tap Pay. 2. Turn on aeroplane mode before the confirmation appears. 3. Turn it off again. 4. Open Orders.
  • Expected: The app says the payment may not have gone through and how to check. Orders shows at most one order. Paying again does not charge twice.

The offline case that finds real bugs isn't opening the app in aeroplane mode. It's losing the signal halfway through something that matters. The expected result accepts that the app may not know what happened, and asks only that it says so and never charges twice.

PERM-02: Camera denied is survivable (Android, iOS; N/A on web)

  • Preconditions: Signed in. Camera access denied for the app in system settings.
  • Steps: 1. Open Profile. 2. Tap Change photo. 3. Choose Take photo.
  • Expected: A message says camera access is off, with a button that opens the app's settings and an option to pick from the photo library instead. No crash, and no repeated prompt.

Permissions have three states: granted, denied, and granted later in settings. Teams test the first. This tests the second.

What a good test case looks like

  • It checks one behaviour. If the title needs an "and", it's probably two cases.
  • It stands on its own. Anything it depends on goes in the preconditions, not in "run case 6 first".
  • It gives the same answer every time. Two people on the same build should reach the same result.
  • One platform doesn't stand in for all of them. A pass on an iPhone says nothing about Android.

Organising them

Group cases by journey (sign-up, password reset, checkout, offline, permissions), not by who wrote them or which sprint they arrived in. Give each section a short reference like PAY and number the cases inside it. New cases get the next number; old numbers are never reused.

Put the sections that matter most first. If sign-in and payment break, nothing else matters, and that ordering does the job of a priority column at a glance. Keep platform differences in columns, not in three nearly identical copies of the same case.

The template

Here's the shape as Markdown, which pastes straight into a repo, a doc or a spreadsheet:

## Checkout (PAY)

| ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS |
|---|---|---|---|---|---|---|---|
| PAY-01 | Pay with a saved card | Signed in. A saved card that will succeed. One item in the basket | 1. Open the basket. 2. Tap Checkout. 3. Choose the saved card. 4. Tap Pay. | A confirmation shows an order number and the same total as the basket. A receipt email arrives. The basket is empty. | | | |
| PAY-03 | Tapping Pay twice charges once | Signed in. A saved card. One item in the basket | 1. Go to checkout. 2. Tap Pay twice, quickly. | One order is created and the card is charged once. The button shows it is working after the first tap. | | | |
Enter fullscreen mode Exit fullscreen mode

The full version, with all 13 example cases across five sections, is in the original guide: How to write test cases, with examples and a template. Full disclosure: I build qarunbook, a test management tool for web and mobile apps, and that format is what it imports. It doesn't run automated tests; it keeps the cases, the result on each platform, and the bugs they turn up. The template works just as well in a spreadsheet.

If you write test cases differently, I'd like to hear how, especially how you handle expected results for things that can partly succeed, like the dropped-connection payment above.

Top comments (0)