Guide · updated 1 October 2026
How to write test cases (with examples and a free template)
A test case is a short set of instructions that tells someone exactly what to do and exactly what they should see. Here is how to write ones that find bugs, with worked examples you can copy.
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 guide is about writing them so somebody else can run them and reach the same answer you would.
What a test case is
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 does not, 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 that has no account behind it.
- A checklist item is shorter: “password reset works”. It is fine as a reminder for the person who wrote it. It is 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 parts of a test case
Six fields are enough. Each one is there for a reason:
- ID. A short, stable handle like
PAY-02. People quote it in messages and bug reports, so it should never change once it is written, even if the case moves. - Journey (the title). What is being checked, in the words a customer would use: “A declined card is handled”, not “Validate payment error state”. A good title tells you what the case is about without opening it.
- Preconditions. The state you must be in first, including any test data: signed in or out, a saved card, an account that already exists, aeroplane mode on. Many “it doesn’t work” reports come from a tester who started in a different state from the author.
- Steps. Numbered, one action each, written for someone who has never seen the code. Name the buttons as they appear on screen.
- Expected result. What you should see, concretely enough that the tester can decide pass or fail without asking anyone.
- Platforms. One column per platform you ship on (web, Android, iOS), so the same case is recorded separately for each. Write
N/Awhere the feature does not exist on a platform.
Some templates add more: actual result, status, tester, date, priority, postconditions. The first four are not part of writing the case. They are what happens when somebody runs it, and they change on every build, so they belong next to the result rather than in the plan. Priority is better handled by the order of your sections (more on that below). Postconditions, such as deleting the test order afterwards, are worth a line in the steps when leaving the data behind would break the next case.
How to write a test case, step by step
- Start from what the user is trying to do. Read the feature, the user story or the acceptance criteria, and write down the outcome a person wants: “pay for what is in my basket”. That outcome is your scenario.
- List the cases before you write any of them. For each scenario, write 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. This second list is where bugs tend to hide.
- Look at the edges of every input. If a password needs at least 8 characters, test 7 and 8, not 3 and 20. Values either side of a limit find more bugs than values far from it. And when many inputs should behave the same way, one example from each group is enough: you do not need ten different valid email addresses.
- Write the preconditions. Everything the tester needs before step one, including accounts, cards and data. If a case needs a declining card or a used reset link, say so here.
- Write the steps. One action per step, in order, with the on-screen names of buttons and screens. Leave out anything the preconditions already cover.
- Write the expected result. Describe what is on screen, what is kept, what is cleared, and what did not happen (no second order, no email). This is the part that decides whether the case finds anything.
- Mark the platforms. Leave a column blank if the case needs testing there, and
N/Aif the feature does not exist on that platform. - Have someone else run it once. If they ask you a question, the answer belongs in the test case.
Write expected results a tester can judge alone
The expected result is where most 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 different 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.
Test case examples
Five worked examples from the template below, for a typical shopping app on web, Android and iOS. Each one covers a journey nearly every app has, and the note under it says what the case is really checking.
SIGN-01 Sign up with an email address
- Preconditions: Signed out. No account exists for the email you will use
- Steps:
- Open the app.
- Tap Create account.
- Enter a name, a new email address and a password of at least 8 characters.
- Tap Create account.
- Expected result: The home screen opens with the name you entered at the top. A verification email arrives within a minute.
- Platforms: Web, Android, iOS
The preconditions matter here: an email that already has an account turns this into a different case (SIGN-02). The expected result checks two things a quick look would miss: that the name was saved, and that the verification email actually arrives.
RESET-01 Reset a forgotten password
- Preconditions: Signed out. An existing account whose inbox you can open
- Steps:
- Tap Sign in, then Forgot password.
- Enter the account's email and submit.
- Open the link in the reset email.
- Set a new password.
- Sign in with the new password.
- Expected result: The new password signs you in. Signing in with the old password fails with the usual wrong-password message.
- Platforms: Web, Android, iOS
Step 5 and the second half of the expected result are what make 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:
- Open the basket.
- Tap Checkout.
- Enter the declining card.
- Tap Pay.
- Expected result: A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders.
- Platforms: Web, Android, iOS
The refused path of checkout. It needs a card that is set up to decline, so it says so in the preconditions; most payment providers publish test card numbers for exactly this.
NET-02 The connection drops during payment
- Preconditions: Signed in. A saved card. One item in the basket
- Steps:
- Tap Pay.
- Turn on aeroplane mode before the confirmation appears.
- Turn it off again.
- Open Orders.
- Expected result: 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.
- Platforms: Web, Android, iOS
The offline case that finds real bugs is not opening the app in aeroplane mode, it is 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
- Preconditions: Signed in. Camera access denied for the app in system settings
- Steps:
- Open Profile.
- Tap Change photo.
- Choose Take photo.
- Expected result: 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.
- Platforms: Android, iOS (Web marked N/A: the feature does not exist there)
Permissions have three states: granted, denied, and granted later in settings. Teams test the first. This case tests the second, and it is marked N/A on web, where the feature works differently.
The template also has a sign-up with an existing email, a password one character too short, a reset link opened twice, a reset for an unknown email, a successful payment, Pay tapped twice, opening the app offline, and a notification prompt that should wait until it is useful.
What makes a good test case
- It checks one behaviour. If the title needs an “and”, it is probably two cases. When a case covering three things fails, nobody knows which one broke.
- It stands on its own. It can be run without running another case first. Anything it depends on goes in the preconditions.
- It gives the same answer every time. Two people running it on the same build should reach the same result.
- It uses the app’s words. Buttons and screens named as they appear, not as they are called in the code. No endpoints, no file paths.
- It is short. If a case runs past ten or so steps, it is usually a journey that should be split, or set-up that belongs in the preconditions.
- It will still be true next month. Avoid details that change every release, such as exact prices or today’s date, unless they are the point of the case.
Common mistakes
- Only the happy path. The demo path gets tested and the rest ships untested. For each important journey, write the refused versions too.
- “Works as expected” as the expected result. It is not a result, it is a hope. Say what you should see.
- Missing preconditions. The author was signed in with a saved card; the tester was not. Both are right, and the case is useless.
- Several actions in one step. “Fill in the form and submit” hides which field caused the problem.
- Cases that depend on each other. If case 7 only works after case 6, one failure takes out both, and nobody can run them out of order.
- Written for the developer. “Verify the 402 response renders” means nothing to the person testing the app on their phone.
- Never updated. The app changes and the cases do not. When a case fails because the screen moved, not because of a bug, fix the case that day.
- One platform standing in for all of them. A pass on an iPhone says nothing about Android. Record each platform separately.
How to organise test cases into sections
Group cases by the journey they belong to, not by who wrote them or which sprint they arrived in: sign-up, password reset, checkout, offline, permissions. A tester can then take one section and work through it on one device without jumping around the app.
- Give each section a short reference, like
PAY, and number the cases inside it:PAY-01,PAY-02. When a section grows, new cases get the next number; old numbers are never reused. - Put the sections that matter most first. Sign-up, sign-in and payment are usually at the top, because if those break nothing else matters. That ordering does the job a priority column would, and it is visible at a glance.
- Keep platform differences in the columns, not in copies. One case with web, Android and iOS columns is easier to keep up to date than three nearly identical cases.
- Keep a short set of cases that must never break, and run it on every build. The release checklist covers how to choose it.
How many cases is enough depends on the app, not on a number. A useful test: if you could finish the whole plan and still be surprised by an obvious bug, a journey is missing. The test plan gap checker will point out common ones.
Manual or automated?
The same test case can be run by a person or turned into an automated test. Which is better depends on the case:
- Run it by hand when it needs judgement (does this message make sense, does the screen look right), when it involves a real device (camera, push notifications, a call interrupting the app), when the feature is new and still changing, or before a release, when you want someone to use the app the way a customer will.
- Automate it when it is stable, runs on every build, and has a result a machine can check, such as a total that must match or a page that must load. Automation is good at repetition and poor at noticing something odd it was not told to look for.
Most teams need both. A well-written manual test case is also the clearest specification for an automated one: the preconditions become the set-up, the steps become the actions, and the expected result becomes the assertions. qarunbook is for the manual side: it does not run automated tests. It is where the cases, the results per platform and the bugs they turn up are kept.
The template
Every example above, plus eight more, in the format qarunbook imports. Copy it, swap the shop app’s journeys for yours, and keep the shape: a heading per section with its reference in brackets, one row per case, a column per platform.
# Shop app — example test cases ## Sign up (SIGN) | ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS | |---|---|---|---|---|---|---|---| | SIGN-01 | Sign up with an email address | Signed out. No account exists for the email you will use | 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. | The home screen opens with the name you entered at the top. A verification email arrives within a minute. | | | | | SIGN-02 | Sign up with an email that already has an account | Signed out. An account already exists for the email | 1. Tap Create account. 2. Enter that email, a name and a valid password. 3. Tap Create account. | A message says an account already exists for this email and offers Sign in and Forgot password. No second account is created. | | | | | SIGN-03 | A password that is too short is refused | Signed out | 1. Tap Create account. 2. Enter a name, a new email and a 7-character password. 3. Tap Create account. | A message beside the password field gives the minimum length. The name and email are still filled in. No account is created. | | | | ## Password reset (RESET) | ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS | |---|---|---|---|---|---|---|---| | RESET-01 | Reset a forgotten password | Signed out. An existing account whose inbox you can open | 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. | The new password signs you in. Signing in with the old password fails with the usual wrong-password message. | | | | | RESET-02 | A reset link only works once | A reset link that has already been used to set a new password | 1. Open the same link again. | A page says the link has expired or been used, with a way to ask for a new one. The password is not changed. | | | | | RESET-03 | Reset for an unknown email gives nothing away | Signed out. No account exists for the email | 1. Tap Forgot password. 2. Enter the unknown email and submit. | The same confirmation appears as for a real account. No email arrives, and nothing on screen says the account does not exist. | | | | ## 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-02 | A declined card is handled | Signed in. A test card that will decline. One item in the basket | 1. Open the basket. 2. Tap Checkout. 3. Enter the declining card. 4. Tap Pay. | A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders. | | | | | 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. | | | | ## Offline (NET) | ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS | |---|---|---|---|---|---|---|---| | NET-01 | Open the app with no connection | Signed in. The app opened once before. Aeroplane mode on | 1. Open the app. | A clear no-connection message with a Retry button, not an endless spinner. Anything already loaded is still shown. | | | | | NET-02 | The connection drops during payment | Signed in. A saved card. One item in the basket | 1. Tap Pay. 2. Turn on aeroplane mode before the confirmation appears. 3. Turn it off again. 4. Open Orders. | 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. | | | | ## Permissions (PERM) | ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS | |---|---|---|---|---|---|---|---| | PERM-01 | Notifications are asked for at the right moment | A fresh install | 1. Open the app. 2. Sign up. 3. Place a first order. | No prompt on first launch. The prompt appears after the first order, with a line saying the notifications are for order updates. | N/A | | | | PERM-02 | Camera denied is survivable | Signed in. Camera access denied for the app in system settings | 1. Open Profile. 2. Tap Change photo. 3. Choose Take photo. | 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. | N/A | | |
Or take the file: Markdown·CSV·Excel·no email, no sign-up.
There are more templates on the templates page: a UAT test plan, a mobile app checklist, a release pass, a retest log, and a blank one to fill in.
Import your test cases into qarunbook
A spreadsheet holds test cases well. It is less good at what happens next: who ran each case, on which platform, which bug it turned up, and whether the fix has been checked again. That is what qarunbook is for.
- Create a free account. No card is needed.
- Choose Add app, name it, and upload the template as Markdown, CSV or Excel. It is read exactly as written: each section becomes a section, each row a case, each platform column a column.
- If your test cases are already written in your own layout, in a spreadsheet, a Word document or a PDF, use the AI import instead. It reads them into a preview, and you review every case before anything is saved.
- Invite the people testing. Each result is recorded per platform with the name of whoever recorded it, and when a bug is marked fixed, the case asks for a retest rather than going back to passed on its own.