A test case that says 'verify the login works' is useless: a tester can't execute it because 'works' isn't defined, and a developer can't automate it because there's nothing concrete to assert. A well-written test case is a contract with three things pinned down: the exact preconditions, the exact steps, and a specific observable expected result. This guide covers all required components with complete examples, the design techniques that make coverage systematic, and when to write formal test cases versus testing exploratorily.

What a test case is not

A test case is not a list of clicks. "Go to the login page. Enter username. Enter password. Click submit. Check the page." This tells you what to do but not what to verify, what the expected behavior is, or what "correct" looks like.

A test case is a contract: given these conditions and these actions, this specific outcome should occur.

The components of a test case

Every test case needs these fields. The exact tool or template doesn't matter (TestRail, Zephyr, Notion, a spreadsheet), but these elements should be present somewhere.

Test case ID: A unique identifier. TC-001, AUTH-005, ITEMS-032, whatever your team uses. Allows referencing in bug reports and traceability matrices. Title: One sentence describing what is being tested. Same rules as bug report titles: specific, no vague words. Preconditions: What must be true before the test starts. User account status, data that must exist, system state, environment. Test steps: The exact actions to perform, numbered, in order. Each step is one action. Test data: The specific inputs used. Not "enter a valid email" but "enter admin@becomeqa.com." Expected result: What should happen after the final step. Specific, observable, verifiable. Priority: How critical this test case is. High (blocks release if it fails), Medium (important but has a workaround), Low (nice to have). Status: Pass, Fail, Blocked, Not Executed.

A minimal test case

ID: AUTH-001
Title: User can log in with valid credentials

Preconditions:
- User account admin@becomeqa.com exists with password testpass123
- User is not currently logged in

Steps:
1. Navigate to https://lab.becomeqa.com
2. Click the "Login" button in the navigation
3. Enter "admin@becomeqa.com" in the Username field
4. Enter "testpass123" in the Password field
5. Click "Submit"

Expected result:
- Modal closes
- Dashboard page is displayed
- "My Travel Items" heading is visible
- Navigation shows the logged-in user's name or avatar

Priority: High

This is complete. A new QA engineer could execute it without asking questions. A developer could write an automated test from it directly.

How to write test cases for the login feature

Login has more test cases than the happy path. A complete login feature requires at minimum:

Happy path:
  • Valid credentials → successful login
Negative paths:
  • Incorrect password → error message, no redirect
  • Non-existent username → error message (careful: don't reveal whether the email exists)
  • Empty username → validation error
  • Empty password → validation error
  • Both empty → validation error
Boundary cases:
  • Maximum length email address
  • Maximum length password
  • Password with special characters (!@#$%^&*)
Security cases:
  • SQL injection in username field → should not break the application
  • Script injection () → should be escaped, not executed
UX cases:
  • Password field should mask characters
  • Pressing Enter in the password field should submit the form
  • "Forgot password" link should be present and functional

That's 12+ test cases for one feature. Deciding how many to write is a judgment call based on risk. A payment provider's login needs all of them, an internal admin tool used by 5 people needs fewer.

Test case design techniques

Equivalence partitioning

Instead of testing every possible age from 0 to 120, divide the input space into groups that should behave identically. Test one value from each group.

For an age field that accepts 18–65:

  • Valid partition: any value 18–65 (test one: 35)
  • Invalid under: any value below 18 (test one: 15)
  • Invalid over: any value above 65 (test one: 70)
  • Invalid type: non-numeric input (test one: "twenty")

Four test cases instead of 120.

Boundary value analysis

Bugs cluster at boundaries. Test the edges of each partition.

For the same 18–65 age field:

  • 17: just below lower bound (invalid)
  • 18: lower bound (valid)
  • 65: upper bound (valid)
  • 66: just above upper bound (invalid)

Combined with equivalence partitioning: 4 well-chosen values cover the entire range.

Decision table testing

When multiple conditions combine to produce different outcomes, a decision table maps all combinations:

| Logged in | Has active subscription | Shows premium content |

|-----------|------------------------|----------------------|

| No | any | No |

| Yes | No | No |

| Yes | Yes | Yes |

Write one test case per row. Decision tables are excellent for business rules with multiple conditions.

Writing test cases from a user story

User story: "As a user, I can add a travel item with a destination name and status."

This is a starting point, not a complete specification. Before writing test cases, ask: What are the valid statuses? Is destination required? What's the max length? What happens if you submit an empty form?

Get answers first, then write test cases. Test cases written from ambiguous requirements are usually wrong. Not because the tester is incompetent, but because the requirement didn't specify the behavior.

ID: ITEMS-001
Title: User can create a new travel item with valid data

Preconditions:
- User is logged in as admin@becomeqa.com
- Dashboard is displayed with the items table

Steps:
1. Click "Add Item" button
2. Enter "Tokyo" in the Destination field
3. Select "Planned" from the Status dropdown
4. Click "Save"

Expected result:
- Modal closes
- New row "Tokyo" with status "Planned" appears in the items table
- Success confirmation is displayed (toast or message)

Priority: High

ID: ITEMS-002
Title: Add item form shows validation error when destination is empty

Preconditions:
- User is logged in as admin@becomeqa.com
- Add item modal is open

Steps:
1. Leave the Destination field empty
2. Select "Planned" from the Status dropdown
3. Click "Save"

Expected result:
- Form does not submit
- Validation error "Destination is required" appears below the Destination field
- Modal remains open

Priority: High

What makes a test case good vs bad

Good:
  • Specific enough to reproduce without asking questions
  • Expected result is observable and verifiable (not "page works correctly")
  • Preconditions are complete
  • One expected result per test case
  • Steps are sequential and unambiguous
Bad:
  • "Verify that the login works" (what does "works" mean?)
  • Missing preconditions (the test will fail because the data doesn't exist)
  • Steps combined into one ("fill in the form and submit"), too vague
  • Multiple expected results in one test case (if one fails, you don't know which condition failed)
A test case should be executable by someone who has never seen the feature before. If you find yourself adding mental context when you re-read your steps, the test case isn't specific enough.

When to write test cases vs when to test exploratorily

Write test cases for: regression scenarios that will be re-executed repeatedly, high-risk or complex features, anything that needs sign-off from a product owner or stakeholder, and flows that will eventually be automated.

Test exploratorily for: new features before test cases are written (learn the feature first), edge cases you discover while testing (follow the thread), anything time-boxed where writing scripts would take longer than executing them.

Both approaches have a place. The best QA engineers know when to use which.

FAQ

How many test cases are enough for a feature?

Enough to give you confidence that the feature works correctly across its main paths, edge cases, and error conditions. There's no fixed number. A simple form with two fields needs fewer than a complex multi-step checkout flow.

Should I write test cases before or after development?

Before, or during. Writing test cases from requirements before development starts serves two purposes: it forces clarification of ambiguous requirements, and it produces a ready-made testing checklist when development is done. This is the shift-left approach.

My test cases keep failing because the requirements changed. What do I do?

Update the test cases as requirements change. Test cases that reflect outdated requirements are worse than useless. They give false confidence. Treat test cases as living documentation, not immutable artifacts.

Do I need to write test cases for automated tests?

Automated tests are a form of executable test cases. If your automated test is well-written and readable, it is the test case. You don't need a separate manual document duplicating what the code already says.

→ See also: The Anatomy of a Bug Report That Developers Actually Fix | Risk-Based Testing: Prioritizing What to Test When You Can't Test Everything | Test Case vs. Test Scenario: What's the Difference and When to Use Each | Test Case Design Techniques: EP, BVA, Decision Tables, State Transition | How to Write a Test Plan: Template and Real Examples