How to Write Test Cases
A test case is a precise, repeatable instruction: given these preconditions, do these steps with this data, and expect this result. Written well, it lets any tester — or an automation script — verify one behaviour the same way every time and tells a developer exactly how to reproduce a failure. Written badly, it is a vague sentence that passes on Monday and fails on Tuesday for reasons nobody can name.
This guide gives you the structure of a good test case, a template you can paste into Jira, TestRail or a spreadsheet, a worked example, and the design techniques that turn a requirement into a small set of cases with real coverage.
The fields every test case needs
- ID — a stable identifier (TC-LOGIN-004) so the case can be referenced from bugs and reports.
- Title — one line that names the behaviour, not the feature: "Login rejects a wrong password with a clear message", not "Login test".
- Preconditions — the state the system must be in before step 1: an existing verified user (a temporary inbox can receive the verification email when you create one), an empty cart, a specific role.
- Test data — the exact values used, or a pointer to a generated data set. "A valid email" is not test data; "layla.h@example.com" is.
- Steps — numbered, one action each, written so a new hire could follow them.
- Expected result — what must be observed after the last step, specific enough to be checked: status code, message text, record state.
- Priority and type — smoke, regression, negative, boundary — so suites can be filtered.
- Traceability — the requirement, user story or acceptance criterion the case covers.
A test case template
ID: TC-LOGIN-004
Title: Login rejects a wrong password with a clear message
Priority: High Type: Negative
Requirement: US-12 (User can sign in with email and password)
Preconditions: A verified account exists for layla.h@example.com
Test data: email = layla.h@example.com, password = WrongPass!9
Steps:
1. Open /login
2. Enter the email and the wrong password
3. Click "Sign in"
Expected result:
- The page stays on /login
- The message "Email or password is incorrect" is shown
- No session cookie is set; the failed attempt is counted
Postconditions: Account is not locked (attempt 1 of 5)Test scenario vs test case
A test scenario is a one-line statement of a situation to test: who does what, under which condition, and which outcome matters — "A registered user tries to sign in with a wrong password and stays signed out". A test case is the detailed script for one variation of that scenario, with exact data, steps and an expected result. One scenario usually expands into several cases that vary the data or the system state but share its outcome: a wrong password, the correct password typed with Caps Lock on, the old password after a reset. TC-LOGIN-004 in the template above is one of them. "Verify login with valid and invalid credentials" is too broad for one scenario: a valid sign-in, a wrong password and a locked account end differently, so each is a scenario of its own, with its own cases. Start with scenarios to agree on scope, then write cases for the variations that matter.
Design techniques that find real bugs
- Equivalence partitioning — group inputs that the system should treat the same (ages 18–65) and test one value per group instead of every value.
- Boundary value analysis — most defects live at the edges. For a field that accepts 1–100, test 0, 1, 2, 99, 100 and 101.
- Decision tables — when several conditions combine (member? coupon? over £50?), a table of every combination guarantees no rule is forgotten.
- State transition testing — for anything with a lifecycle (order: created → paid → shipped → refunded), test every allowed transition and the forbidden ones.
- Negative testing — wrong types, missing fields, special characters, very long input, concurrent actions, expired sessions.
- Pairwise testing — when there are too many parameter combinations, cover every pair of values with a fraction of the cases.
Axis shortened between 2 and 99: not to scale
- ≤ 0Invalid
- 1–100Valid
- ≥ 101Invalid
- 10 Just below the minimumExpected: rejected
- 21 MinimumExpected: accepted
- 32 Just above the minimumExpected: accepted
- 499 Just below the maximumExpected: accepted
- 5100 MaximumExpected: accepted
- 6101 Just above the maximumExpected: rejected
The three groups are equivalence partitioning applied to the same field, which tests one value per group; the six values sit at the groups’ edges, where most defects live.
Try it: Boundary Value Generator Try it: Equivalence Partition Generator Try it: Decision Table Generator Try it: Negative Test Generator Try it: AI Negative Scenario Generator Try it: AI Edge Case Generator
From a user story to test cases
Start from the acceptance criteria. Each Given/When/Then statement is at least one positive case; each "should not" or "must" hides a negative one. Then add the boundaries of every field the story mentions, one case for each user role that touches the feature, and one for what happens when a dependency (payment provider, email service) fails. A story with three acceptance criteria typically yields eight to fifteen cases.
Try it: User Story → Test Cases Try it: Acceptance Criteria Generator Try it: AI Test Case Generator
Common mistakes
- Testing several things in one case — when it fails, nobody knows which part broke.
- Vague expected results ("works correctly", "error shown") that cannot be judged pass or fail.
- Steps that depend on another case having run first, so the suite cannot be run in any order.
- Test data that is real customer data — use generated data that is realistic but synthetic.
- Writing only the happy path. Most production incidents are negative or boundary cases.
- Never pruning: cases for removed features and duplicates make regression runs slow and untrusted.
Try it: Test Data Factory Try it: AI Duplicate Test Case Detector
Frequently asked questions
What is the difference between a test case and a test scenario?
A scenario describes in one line a situation to test and the outcome that matters; a test case is the step-by-step script for one variation of that scenario, with exact data and an expected result. Situations that end differently, such as a valid sign-in and a locked account, are separate scenarios.
How many test cases should a feature have?
Enough to cover each acceptance criterion, each boundary of each input, each role and the main failure modes — usually 8–15 for a typical user story. Coverage of behaviours matters more than the count.
Should test cases include test data?
Yes, or a reference to a generated data set. Without exact values a case is not repeatable and failures are hard to reproduce.
What format do Jira, TestRail and Xray expect?
All of them import CSV with columns for title, preconditions, steps, expected result and priority. Keep one step per row when the tool supports step tables.
Can AI write test cases?
AI can draft a strong first set from a user story or requirement, including negative and edge cases that reviewers often skip. A tester should still review each case for business context and remove anything that does not apply.
How do I write test cases for an API?
The same structure applies: the steps are requests, the test data is the payload and headers, and the expected result is the status code, response body and any state change. See the API testing guide for a full method.
References
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- ISO/IEC/IEEE 29119-1:2022 — Software and systems engineering — Software testing — Part 1: General conceptsThe international software-testing standard: concepts, processes, documentation and techniques.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
Practice exercise
Write the test cases for a transfer between own accounts
You test the mobile app of a fictional retail bank. User story PAY-31 is ready for test design, and a colleague has already drafted one test case for it. The story, a note on how transfers work and the draft are below.
First review the draft against the fields every test case needs and the common mistakes in the guide. Then replace it with test cases that another tester could run without asking you anything: write two in full with the guide’s template, and derive the rest of the set as a list with one line per case. Where the story does not tell you an expected result, do not guess: write it down as a question for the product owner.
Pen and paper are enough; allow about 30 minutes. Afterwards you can check your amount values with the Boundary Value Analysis tool, which handles decimal values, take amount formats for extra negative cases from the Decimal Dataset Generator, and paste AC1 to AC3, one per line, into Requirements → Test Cases to compare its traceability matrix with yours.
Fictional retail bank, mobile app. All accounts are in SAR.
Transfers are posted by the core banking system, which the app calls when the customer
taps "Confirm".
The screen "Between my accounts" (under "Transfers") has the fields "From", "To" and
"Amount" and the button "Confirm".
A customer also sees accounts that another customer shared with them with view-only
access; those accounts are not their own.
PAY-31 Transfer money between my own accounts
As a customer with more than one account
I want to move money between my own accounts
so that I can pay from the account I choose.
AC1 Given I am signed in and hold at least two active accounts, when I choose a source
account, a different destination account and an amount and tap "Confirm", then the
amount is debited from the source and credited to the destination at once, and a
confirmation with a reference number is shown.
AC2 The amount must be from 1.00 to 20,000.00 SAR, both included, with at most two
decimal places. Otherwise "Confirm" stays disabled and the message
"Enter an amount from 1.00 to 20,000.00 SAR" is shown under the field.
AC3 A transfer must not take the source account below zero. When I confirm an amount
higher than the available balance of the source account, the message
"Insufficient balance" is shown and no money moves.
Draft test case written by a colleague
Title: Transfer test
Steps: 1. Log in as Ahmed Al-Harbi (his accounts were copied from production).
2. Run TC-DEP-03 first so that his savings account has money.
3. Transfer some money from savings to current, then try a big amount
and an amount bigger than the balance.
Expected: Transfer works correctly and an error is shown.- Review the draft field by field — ID, title, preconditions, test data, steps, expected result, priority and type, traceability — and note what is missing or weak. Name each common mistake from the guide that it makes.
- Count the behaviours the draft tests, and split it so that each case checks one behaviour.
- Write two cases in full with the guide’s template: the valid transfer (AC1), and a transfer of 0.01 SAR more than the available balance (AC3). Use exact, synthetic test data and expected results that anyone would judge pass or fail the same way.
- Derive the rest of the set with the guide’s method for user stories: a positive case for each acceptance criterion, a negative case for each "must" and "must not", the boundaries of every number, one case for each kind of user or account, and one for a failure of the core banking system. Write one line per case: ID, type, criterion, test data and expected result.
- Check that every acceptance criterion is covered by at least one case, and list your questions for the product owner.
Show the model answer
Review of the draft (step 1). Your wording may differ; seven or more of these findings is a full answer. (1) No ID, so bug reports and test reports cannot refer to the case. (2) The title "Transfer test" names the feature, not a behaviour. (3) No preconditions: the starting balances are not stated, so nobody can say which balances to expect afterwards. (4) Step 2 makes the case depend on TC-DEP-03 having run first, so the suite cannot run in any order; the balance the case needs belongs in its preconditions. (5) The customer and his accounts were copied from production: real customer data has no place in test cases, least of all in banking; use a synthetic test customer. (6) The test data is vague: "some money" and "a big amount" are not values, so two testers would run two different tests. (7) Step 3 holds several actions and three behaviours — a valid transfer, the 20,000.00 SAR limit and the balance check — so when it fails, nobody knows which part broke. (8) "Transfer works correctly and an error is shown" cannot be judged pass or fail: which balances, which message, and is a reference number shown? (9) No priority, no type and no traceability to PAY-31 and its criteria. Four of these are common mistakes named in the guide: several things tested in one case (7), a vague expected result (8), a step that depends on another case (4) and real customer data (5).
Split (step 2): the draft tests three behaviours, so it becomes three cases — the valid transfer (TC-PAY31-01), an amount above the limit (TC-PAY31-05) and an amount above the balance (TC-PAY31-09). Each negative case breaks exactly one rule: the limit cases transfer from the savings account, whose balance is above 20,000.00 SAR, and the balance cases use amounts inside the limit, so a failure points to one rule. All cases start from the same synthetic data set, restored before each run: test customer CUST-T-031 with savings account SAV-2002 at 25,000.00 SAR and current account CUR-1001 at 5,000.00 SAR, both active, with no pending transactions; TC-PAY31-11 and TC-PAY31-12 add the shared account and the second test customer they name.
TC-PAY31-01 in full (step 3). Title: A transfer between own accounts moves the amount and shows a reference number. Priority: High. Type: Positive, also a smoke candidate. Requirement: PAY-31 AC1. Preconditions: CUST-T-031 is signed in; SAV-2002 has an available balance of 25,000.00 SAR and CUR-1001 has 5,000.00 SAR. Test data: source SAV-2002, destination CUR-1001, amount 250.00. Steps: 1. Open "Transfers". 2. Choose "Between my accounts". 3. Select SAV-2002 in "From". 4. Select CUR-1001 in "To". 5. Enter 250.00 in "Amount". 6. Tap "Confirm". Expected result: a confirmation with a reference number is shown; SAV-2002 shows 24,750.00 SAR and CUR-1001 shows 5,250.00 SAR. Postconditions: both balances have changed; restore the data set before the next run.
TC-PAY31-09 in full. Title: A transfer 0.01 SAR above the available balance is refused with "Insufficient balance". Priority: High. Type: Negative, boundary. Requirement: PAY-31 AC3. Preconditions: CUST-T-031 is signed in; CUR-1001 has an available balance of 5,000.00 SAR and SAV-2002 has 25,000.00 SAR. Test data: source CUR-1001, destination SAV-2002, amount 5000.01 — the balance plus 0.01, and inside the AC2 limit, so only AC3 is broken. Steps: 1. Open "Transfers". 2. Choose "Between my accounts". 3. Select CUR-1001 in "From". 4. Select SAV-2002 in "To". 5. Enter 5000.01 in "Amount". 6. Tap "Confirm". Expected result: the message "Insufficient balance" is shown; no confirmation and no reference number appear; CUR-1001 still shows 5,000.00 SAR and SAV-2002 still shows 25,000.00 SAR. Postconditions: none, the balances are unchanged.
The rest of the set (step 4), one line per case; with the two above it makes 14 cases. TC-PAY31-02 [Boundary · AC2] Amount 1.00, the lower edge, from SAV-2002: accepted; SAV-2002 shows 24,999.00 and CUR-1001 shows 5,001.00. TC-PAY31-03 [Boundary · AC2] Amount 0.99: "Confirm" stays disabled, the message "Enter an amount from 1.00 to 20,000.00 SAR" is shown under the field, and no balance changes. TC-PAY31-04 [Boundary · AC2] Amount 20000.00, the upper edge, from SAV-2002: accepted; SAV-2002 shows 5,000.00 and CUR-1001 shows 25,000.00. TC-PAY31-05 [Boundary · AC2] Amount 20000.01 from SAV-2002: "Confirm" stays disabled and the same message is shown; the balance of 25,000.00 makes sure only the limit is broken. TC-PAY31-06 [Negative · AC2] Amount 10.005, with three decimal places: not accepted; whether the message appears or the third decimal cannot be typed at all is Q1. TC-PAY31-07 [Negative · AC2] "Amount" left empty: "Confirm" stays disabled (whether the message also appears is part of Q1).
TC-PAY31-08 [Boundary · AC3] Amount equal to the available balance, 5000.00 from CUR-1001: accepted, because AC3 forbids going below zero, not reaching it; CUR-1001 shows 0.00 and SAV-2002 shows 30,000.00. TC-PAY31-10 [Negative · AC1] Once SAV-2002 is chosen in "From", it cannot be chosen in "To"; how this is shown is Q3. TC-PAY31-11 [Negative · role] An account that another customer shared with CUST-T-031 with view-only access (VIEW-3003) is not offered in "From"; whether it is offered in "To" is Q4. TC-PAY31-12 [Negative · AC1 precondition] Test customer CUST-T-032 holds one active and one closed account: the closed account is not offered and no transfer can be made; what the customer sees is Q5. TC-PAY31-13 [Negative · dependency] The core banking system does not respond after "Confirm" (simulated in the test environment, for example with a stub): no confirmation is shown, and there is no half-done transfer — either both balances change once or neither changes; the message and the retry behaviour are Q6. TC-PAY31-14 [Negative · AC1, concurrent actions] "Confirm" tapped twice quickly with 250.00 from SAV-2002 to CUR-1001: exactly one transfer and one reference number; SAV-2002 shows 24,750.00, not 24,500.00, and CUR-1001 shows 5,250.00. If you follow the guide’s six-value boundary pattern (three values at each edge), also add 1.01 and 19999.99 from SAV-2002 and, at the balance edge, 4999.99 from CUR-1001; all three are accepted.
Coverage (step 5): AC1 is covered by TC-PAY31-01, 10, 12, 13 and 14; AC2 by 02 to 07; AC3 by 08 and 09; the view-only role by 11. Every criterion has at least one positive and one negative case. The guide expects about eight to fifteen cases for a story with three criteria; what makes a set complete is not the count but that every criterion, every boundary, every kind of user and the failure of the dependency is covered.
Questions for the product owner: Q1 Is a third decimal place blocked while typing, or accepted and answered with the AC2 message? Does that message also appear while "Amount" is still empty? Are amounts with a thousands separator (1,000.00) or with Arabic-Indic digits accepted? Q2 Does the "available balance" in AC3 exclude pending card payments and holds? Q3 Is the source account removed from "To", or is a message shown? Q4 Can money be sent to a view-only shared account, or is it hidden from both lists? Q5 What does a customer with fewer than two active accounts see under "Between my accounts"? Q6 When the core banking system does not answer, which message is shown, may the customer retry, and what stops a retry from moving the money twice? Q7 Is 20,000.00 SAR per transfer the only limit, or is there also a daily total?
Check your own work: the title of each full case should name one behaviour, and each line of your list should check one behaviour; each case should start from stated preconditions, not from another case, use exact synthetic values, and have an expected result that two testers would judge the same way. In ISTQB terms, PAY-31 and its criteria are the test basis, each line of your list covers a test condition, and each full case has the preconditions, inputs, expected results and postconditions that the ISTQB glossary lists in its definition of a test case. If your set did not have TC-PAY31-08 (amount equal to the balance), TC-PAY31-13 (dependency failure) or TC-PAY31-14 (double tap), add them: the first is the edge that AC3 allows, the second is the dependency failure that the guide’s method for user stories asks for, and the third is one of the concurrent actions in the guide’s list of negative tests.
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.
Part of the QA Roadmap
Before this topic
Tools mentioned in this guide
Generate structured test cases (id, title, preconditions, steps, expected result, priority) from a feature description and field list.
Generate high-level test scenarios covering functional, negative, security, usability and compatibility angles for a feature.
Convert a user story (As a / I want / So that) into structured test cases using templates and heuristics.
Generate negative test cases for form fields and APIs: empty, null, wrong type, too long, injection, invalid format.
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Derive valid and invalid equivalence classes for each input and pick representative test values.
Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.
Generate Given/When/Then acceptance criteria and a definition-of-done checklist from a user story.
Turn a user story into professional, structured test cases (ID, title, type, priority, preconditions, data, steps, expected result) with Mutqan AI.
One-stop factory for synthetic test data: users, customers, companies, addresses, numbers, dates and strings in JSON, CSV, SQL or XML.