A test case is not a recipe of clicks. It is an experiment with a clear hypothesis, a controlled setup, and a binary outcome. Write it that way and your test cases survive code reviews, catch real defects, and become useful documentation for the next person on the team.
The anatomy of a good test case
Every test case, whether manual or automated, has eight parts. The format is IEEE 829, modernised. You do not need a fancy tool: a spreadsheet, a JIRA template, or a markdown file works fine.
| Field | What goes in it |
|---|---|
| ID | Unique identifier, e.g. TC-LOGIN-007. |
| Title | One sentence describing the scenario from the user's point of view. |
| Preconditions | Required state before the test starts: user exists, account is verified, OTP is set up. |
| Test data | The specific values used: email, phone, amounts, dates. Never “valid email.” Always [email protected]. |
| Steps | Numbered, one action per step. No assertions inside steps. |
| Expected result | What should happen after the last step. Binary: pass or fail. |
| Priority | P0 (smoke), P1 (regression), P2 (corner cases), P3 (nice to have). |
| Tags | Component, owner, automation status: @login @web @automated. |
The test case lifecycle
A test case moves through six stages. Knowing the stage tells you whether it is worth maintaining.
Six rules that separate good test cases from bad ones
- One scenario per test case.If your title contains “and,” split the case.
- Concrete data, not placeholders.“Enter a valid email” is a bug. “Enter
[email protected]” is a test. - Independent of every other test case. If you have to read TC-031 to understand TC-032, fix TC-032.
- One assertion per expected result. If you assert three things, you are running three tests under one ID and the report cannot tell you which one failed.
- Negative cases are first-class. Submitting an empty form, an invalid OTP, a duplicate email: these are not afterthoughts. Roughly half a mature suite is negative cases.
- Write for someone who has never seen the app.The steps should be runnable by a new joiner on day one.
Worked example 1: UPI login with OTP
ID: TC-LOGIN-007
Title: Successful login with valid mobile + correct OTP
Component: @auth @web @mobile
Priority: P0
Automated: yes (pytest, file: tests/auth/test_login.py)
Preconditions
- A user account exists for +91 98765 43210
- The account is in 'active' state
- The user has KYC status 'verified'
- The OTP sandbox is reachable
Test data
- Mobile: +91 98765 43210
- OTP: 123456 (sandbox returns this for any number)
Steps
1. Open https://juice.upcode.in/login
2. Enter mobile number +91 98765 43210
3. Tap 'Send OTP'
4. Wait up to 5 seconds for the OTP input to appear
5. Enter OTP 123456
6. Tap 'Verify & continue'
Expected result
- The browser navigates to /me
- The greeting "Welcome back" is visible within 3 seconds
- The 'login' event is recorded in the activity log
Negative variants (separate test cases)
- TC-LOGIN-008: empty OTP -> 'Enter all 6 digits' error toast
- TC-LOGIN-009: wrong OTP -> 'Incorrect code' toast, OTP cleared
- TC-LOGIN-010: same OTP submitted twice -> second submit ignored
- TC-LOGIN-011: OTP entered after 10 minutes -> 'OTP expired' errorWorked example 2: Razorpay payment with coupon
ID: TC-CHECKOUT-042
Title: Razorpay UPI payment succeeds with FRESH50 coupon applied
Component: @checkout @payment @razorpay
Priority: P0
Automated: yes (Playwright, file: tests/checkout/coupon.spec.ts)
Preconditions
- Cart contains exactly 1 item: "Fortune Atta 5 kg" priced INR 250
- Coupon FRESH50 is active in the coupon engine
- User is logged in and KYC verified
- Razorpay test mode is enabled (key starts with rzp_test_)
Test data
- Coupon code: FRESH50
- Expected discount: 50% (INR 125)
- Expected payable: INR 125
- UPI VPA for sandbox: success@razorpay
Steps
1. Navigate to /cart
2. Click 'Apply coupon'
3. Enter FRESH50, click 'Apply'
4. Verify discount line shows '-INR 125'
5. Click 'Proceed to pay'
6. Choose UPI > Enter VPA
7. Enter success@razorpay
8. Submit
Expected result
- Order confirmation screen loads
- Order summary shows: subtotal INR 250, discount INR 125, paid INR 125
- Order status in /orders/{id} = 'PAID'
- A row exists in razorpay_payments with status='captured' and amount=12500 (paise)
Negative variants
- TC-CHECKOUT-043: coupon applied then removed -> price returns to INR 250
- TC-CHECKOUT-044: expired coupon EXPIRED50 -> 'Coupon expired'
- TC-CHECKOUT-045: coupon on ineligible item -> 'Coupon not valid for this item'
- TC-CHECKOUT-046: razorpay VPA failure@razorpay -> order status stays 'PENDING'Worked example 3: IRCTC search returns trains
ID: TC-SEARCH-019
Title: Tatkal search for SBC -> MAS on the next day returns at least
one train with Tatkal quota open
Component: @search @booking @irctc
Priority: P1
Preconditions
- The current time is between 09:30 and 11:30 IST
(Tatkal window opens at 10:00 AC, 11:00 non-AC)
- At least one daily train runs between SBC and MAS
Test data
- From station code: SBC (Bengaluru City)
- To station code: MAS (Chennai Central)
- Journey date: tomorrow
- Class: 3A
- Quota: TATKAL
Steps
1. Open the IRCTC search page
2. Type 'Bengaluru' in 'From'; select SBC from the dropdown
3. Type 'Chennai' in 'To'; select MAS
4. Pick tomorrow's date from the calendar
5. Class: 3A, Quota: TATKAL
6. Click Search
Expected result
- The results page loads within 6 seconds
- At least one train row is displayed
- Each row shows train number, name, departure time, arrival time,
and a 'Book Now' button or 'Waitlist' status
- The URL contains the search parameters as query stringWhen to use BDD (Given / When / Then)
Behaviour-Driven Development format is useful when business stakeholders read the test. If your product manager will not read a Gherkin file, do not write one.
gherkinFeature: UPI login Scenario: User logs in with a valid mobile and OTP Given a user is registered with mobile +91 98765 43210 And the user has KYC status "verified" When the user requests an OTP for +91 98765 43210 And submits OTP "123456" Then the user lands on the "/me" dashboard And a "login" event is recorded
BDD shines for a product manager who wants to read the scenarios. It adds friction for an engineering team that already speaks code. Pick the right tool for the audience.
Common anti-patterns
- “Verify the page works properly.” What does properly mean? Be specific.
- A test case with 47 steps. Either split it into a flow of smaller cases or admit it is exploratory and not a repeatable test.
- Assertions in step 4 of 7. The assertion belongs in expected result.
- Copy-pasting an existing case, changing one word, and not updating the ID. You now have two cases with the same coverage and double the maintenance.
- Storing test cases in screenshots of an Excel sheet on a shared drive. Welcome to 2007. Use a real test management system: Xray, Zephyr Scale, TestRail, or a markdown file in Git.
Closing
A test case suite is a long-lived artefact. The cases you write today get read, run, and maintained for years. Spend an extra five minutes on each case to make the steps clear and the expected result binary, and your next QA hire will thank you.
For hands-on practice writing test cases against real apps, our Pro Software Testing program runs you through 50+ realistic scenarios in Phases 4 and 5, with reviewer feedback on every submission.
References
- IEEE 829 Standard for Software and System Test Documentation · The original (now superseded) standard that defined the canonical test case structure.
- ISTQB definition of test case · The shorter authoritative definition.
- Gherkin reference (Cucumber) · BDD syntax, when you want business-readable scenarios.