Test Case Template with Steps, Expected Results & Examples
A standardized test case format with steps, expected results, preconditions, and postconditions for manual and automated testing.
Overview
A test case is a set of conditions under which a tester verifies that an application meets a requirement. The format matters less than the discipline behind it: every test case should be executable by someone who didn’t write it, produce the same result on any machine, and trace back to the requirement it verifies.
The template below covers a login flow end to end: metadata, preconditions, eight steps with expected results, postconditions, test data, six negative cases, five edge cases, a traceability matrix, and a Playwright version of the same case. Copy it, replace the login example, and it works for almost any feature. A second filled-in scenario shows what the same structure looks like for an API endpoint.
When to Use
- Writing manual test cases for a QA cycle or regression run
- Producing test documentation that a compliance audit will read (SOC 2, ISO 27001, FDA)
- Onboarding QA team members to a shared testing standard
- Converting a manual test into an automated script; the step table maps almost 1:1 to code
- Proving coverage: linking each test case to a requirement shows gaps at a glance
If what you need is the bigger picture — scope, environments, entry/exit criteria for the whole effort — that’s a test strategy document, not a test case.
The Template
# Test Case: TC-<ID>
## Metadata
| Field | Value |
|-------|-------|
| Test Case ID | TC-001 |
| Title | Verify user can log in with valid credentials |
| Module | Authentication |
| Feature | Login |
| Priority | Critical |
| Type | Functional |
| Execution Type | Manual / Automated |
| Estimated Time | 5 minutes |
| Created By | <Author> |
| Created Date | 2026-07-05 |
| Last Updated | 2026-07-05 |
| Requirement ID | REQ-AUTH-001 |
| Test Status | Not Run / Pass / Fail / Blocked |
## Preconditions
1. User account exists in the system with email `testuser@example.com`
2. User account is active (not suspended or deleted)
3. User knows the correct password: `Test@1234`
4. Application is accessible at `https://app.example.com`
5. Browser is Chrome 120+ or Firefox 120+
6. No active session for the test user (logged out)
## Test Steps
| Step # | Action | Expected Result | Actual Result | Status |
|--------|--------|-----------------|---------------|--------|
| 1 | Navigate to `https://app.example.com/login` | Login page loads with email and password fields, and a "Log In" button | | |
| 2 | Enter `testuser@example.com` in the email field | Email field displays the entered value | | |
| 3 | Enter `Test@1234` in the password field | Password field displays masked characters (••••••••) | | |
| 4 | Click the "Log In" button | System processes the login request | | |
| 5 | Wait for redirect | User is redirected to the dashboard at `/dashboard` | | |
| 6 | Verify the user avatar | User avatar with initials "TU" is visible in the top-right corner | | |
| 7 | Verify the welcome message | Dashboard displays "Welcome, Test User" | | |
| 8 | Verify session cookie | A session cookie named `session_id` is set with `HttpOnly` and `Secure` flags | | |
## Postconditions
1. User is authenticated and has an active session
2. Session token is valid for 24 hours
3. User activity log records the login event with timestamp and IP
4. User is on the dashboard page
## Test Data
| Field | Value | Notes |
|-------|-------|-------|
| Email | `testuser@example.com` | Pre-created test account |
| Password | `Test@1234` | Changed quarterly |
| Invalid email | `invalid@example.com` | For negative test case |
| Invalid password | `wrongpass` | For negative test case |
## Negative Test Cases
| Test Case ID | Description | Steps | Expected Result |
|--------------|-------------|-------|-----------------|
| TC-001-N1 | Login with invalid password | Same as TC-001 but enter `wrongpass` in step 3 | Error message "Invalid email or password" is displayed. User remains on login page. |
| TC-001-N2 | Login with non-existent email | Same as TC-001 but enter `nobody@example.com` in step 2 | Error message "Invalid email or password" is displayed. No account enumeration. |
| TC-001-N3 | Login with empty email field | Skip step 2, click "Log In" | Email field shows validation error "Email is required" |
| TC-001-N4 | Login with empty password field | Skip step 3, click "Log In" | Password field shows validation error "Password is required" |
| TC-001-N5 | Login with SQL injection in email | Enter `' OR 1=1; --` in email field | Error message "Invalid email format" is displayed. No SQL execution. |
| TC-001-N6 | Login after 5 failed attempts | Repeat TC-001-N1 five times | Account is locked for 15 minutes. Error message "Account locked due to too many attempts." |
## Edge Cases
| Test Case ID | Description | Expected Result |
|--------------|-------------|-----------------|
| TC-001-E1 | Email with leading/trailing spaces | ` testuser@example.com ` should be trimmed and accepted |
| TC-001-E2 | Email with uppercase | `TestUser@Example.com` should be case-insensitive and accepted |
| TC-001-E3 | Password with special characters | Password `Tëst@1234!#$%` should be accepted if it meets policy |
| TC-001-E4 | Session timeout during login | If login takes > 30s, show "Request timeout. Please try again." |
| TC-001-E5 | Concurrent login from two browsers | Second login should succeed. First session should be invalidated. |
## Traceability
| Requirement ID | Requirement Description | Test Case IDs |
|----------------|--------------------------|---------------|
| REQ-AUTH-001 | User can log in with valid credentials | TC-001 |
| REQ-AUTH-002 | Invalid credentials show error without enumeration | TC-001-N1, TC-001-N2 |
| REQ-AUTH-003 | Account locks after 5 failed attempts | TC-001-N6 |
| REQ-AUTH-004 | Session is invalidated on concurrent login | TC-001-E5 |
## Automation Notes
If automating this test case with Playwright:
```typescript
test('TC-001: User can log in with valid credentials', async ({ page }) => {
// Preconditions: ensure test user exists
await ensureTestUserExists();
// Step 1: Navigate to login page
await page.goto('https://app.example.com/login');
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();
await expect(page.locator('[data-testid="password-input"]')).toBeVisible();
await expect(page.locator('[data-testid="login-button"]')).toBeVisible();
// Steps 2-3: Enter credentials
await page.fill('[data-testid="email-input"]', 'testuser@example.com');
await page.fill('[data-testid="password-input"]', 'Test@1234');
// Step 4: Click login
await page.click('[data-testid="login-button"]');
// Step 5: Verify redirect to dashboard
await expect(page).toHaveURL(/\/dashboard/);
// Step 6: Verify user avatar
await expect(page.locator('[data-testid="user-avatar"]')).toHaveText('TU');
// Step 7: Verify welcome message
await expect(page.locator('[data-testid="welcome-message"]')).toHaveText('Welcome, Test User');
// Step 8: Verify session cookie
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'session_id');
expect(sessionCookie).toBeDefined();
expect(sessionCookie.httpOnly).toBe(true);
expect(sessionCookie.secure).toBe(true);
// Postcondition: clean up
await page.click('[data-testid="logout-button"]');
});
## Explanation
The template has six key sections. **Metadata** identifies the case and links it to a requirement. **Preconditions** pin down the starting state. Skip them and the same test passes on your machine and fails on a colleague's, for no reason anyone can see. **Steps** pair one action with one expected result per row; a step with two actions or a vague expectation ("page loads correctly") can't be verified. **Postconditions** check the state after the test, not just the immediate outcome. **Test data** keeps inputs in one place so a password rotation touches one table instead of twenty steps. **Negative and edge cases** live in the same document as the happy path; they're where the bugs actually are, and filing them separately is how they get skipped.
The traceability matrix is the section teams drop first and regret last. It's mandatory evidence for compliance audits, but it pays off even without auditors: a requirement with no test case is a requirement nobody verified. If you generate coverage reports, this matrix is what feeds them; see the [test coverage report template](/docs/test-coverage-report-template/) for the reporting side.
### Detailed Scenario: Test Case for Order Creation API
The same structure works for API testing: swap UI interactions for requests and expected DOM state for status codes and payloads:
```text
API: POST /v1/orders
Module: Orders
Priority: Critical
Type: Functional + Integration
Preconditions:
1. Orders service running in staging
2. User authenticated with valid JWT token
3. At least 3 products with available stock in DB
4. Valid shipping address created beforehand
Steps:
| # | Action | Expected Result |
|---|--------|-----------------|
| 1 | POST /v1/orders with valid body | 201 Created with body {id, status, total} |
| 2 | Verify Location header | Contains /v1/orders/{id} |
| 3 | GET /v1/orders/{id} | 200 OK with same body |
| 4 | Verify stock decremented | GET /v1/products/{sku} shows stock -1 |
| 5 | Verify event published | Message queue has order.created event |
| 6 | Repeat POST with same Idempotency-Key | 200 OK with same order (no duplicate) |
Negative Cases:
| ID | Description | Result |
|----|-------------|--------|
| N1 | Nonexistent customer_id | 400 with VALIDATION_ERROR |
| N2 | Empty items array | 422 with EMPTY_ITEMS |
| N3 | quantity <= 0 | 422 with INVALID_QUANTITY |
| N4 | SKU out of stock | 409 with OUT_OF_STOCK |
| N5 | Missing Authorization header | 401 Unauthorized |
| N6 | Duplicate Idempotency-Key with different body | 409 Conflict |
Edge Cases:
| ID | Description | Result |
|----|-------------|--------|
| E1 | Order with 100 items | 201 Created, latency < 2s |
| E2 | Order with unicode characters in address | 201 Created, data preserved |
| E3 | quantity = 999999 | 201 Created or 422 if exceeds max allowed |
| E4 | Concurrency: 2 orders for same SKU simultaneously | One succeeds, other 409 OUT_OF_STOCK |
Traceability:
| Requirement | Cases |
|-------------|-------|
| REQ-ORD-001 | TC-ORD-001 (happy path) |
| REQ-ORD-002 | TC-ORD-N1..N6 (validation) |
| REQ-ORD-003 | TC-ORD-E4 (concurrency) |
| REQ-ORD-004 | TC-ORD-001 step 6 (idempotency) |
Note step 6: idempotency is a first-class test case for any POST that creates resources, and the negative case N6 (same key, different body → 409) is the one implementations get wrong most often.
How do I document test cases for asynchronous flows?
For asynchronous flows (message queues, webhooks, events), document the expected event as the result instead of an immediate HTTP response. Verify three things: the message published to the queue, the final state after processing, and the maximum acceptable wait. In automation, poll with retry — check every 500ms up to a 10s ceiling — rather than sleeping for a fixed duration.
Should I use BDD (Gherkin) for writing test cases?
BDD (Given-When-Then) earns its keep when product managers or stakeholders have to read the cases. For technical teams, the tabular format is more direct. If you run Cucumber or Behave, Gherkin is required. For Playwright or Vitest, the table maps straight onto code. Keep the table.
How to Adapt This Template
ID conventions. TC-001 for the happy path, TC-001-N* for negatives, TC-001-E* for edge cases keeps every case addressable in one keystroke. Prefix per module (TC-AUTH-*, TC-ORD-*) once you cross a few dozen cases; flat numbering doesn’t scale past a sprint or two.
Prioritize by risk, not by page. A payment flow needs negatives and edge cases spelled out; a “change avatar color” feature needs the happy path and one or two negatives at most. The matrix in the template scales both ways. The discipline is matching case depth to blast radius, not filling every section for every feature.
Fields you can drop. Estimated Time matters for manual regression runs and can go. Execution Type can go once a suite is fully automated. Requirement ID can only go if you’ve got another traceability mechanism. Don’t drop it just because linking is tedious; that’s how coverage holes start.
Fields worth adding for your context. Environment (staging vs prod) for cross-environment QA. Defect ID linking to the bug report when a case fails; the bug reproduction steps template pairs well here. Regression Suite as a yes/no flag if you maintain a separate smoke set.
From manual to automated. Don’t automate everything at once. Candidates first: cases that run every sprint, touch critical paths, and don’t depend on judgment calls. The step table in this template translates row by row: the Playwright example above is the same eight steps as code. When a manual step says “verify the layout looks right,” that’s a visual assertion or a human review, not a selector; automating it blindly is where flaky tests come from, and flaky tests are where the template’s preconditions get blamed first.
Entry and Exit Criteria
A test case documents one verification; a QA cycle documents many. Define when writing and executing cases counts as done, or the cycle drags on forever.
Before writing a case (entry): the requirement is finalized and reviewed, acceptance criteria exist, and test data is available or creatable. Writing cases against requirements that are still moving produces rework, not coverage.
Before starting a run (entry): the environment is up, the build under test is deployed, and the preconditions of each case can actually be satisfied — a suite that can’t set up its own starting state stalls on day one.
To close the cycle (exit): 100% of planned cases executed, all critical and high-priority cases passing, defects triaged with severity, and the test coverage report filled in. Blocked cases get a reason, not a silent skip.
Variants
| Context | Approach | Notes |
|---|---|---|
| API testing | Replace UI steps with HTTP requests | Use status codes and response body assertions (see the order example above) |
| Mobile testing | Add device/OS to preconditions | Test on small and large viewports at minimum |
| Performance testing | Add load parameters and latency thresholds | Use k6 or JMeter scripts |
| Compliance testing | Add regulatory requirement mapping | FDA 21 CFR Part 11, GDPR |
| Exploratory testing | Use charter-based format instead of steps | Time-boxed exploration with debrief |
Exploratory sessions don’t fit this template. Write a charter (mission, scope, timebox) and fill in a findings log instead of steps. The structure above is for repeatable verification, not discovery.
What Works
- Write preconditions explicitly: “user is logged in” is a state, write it down, or the next person runs the case against the wrong starting point
- Keep negative and edge cases in the same document as the happy path; separate files get forgotten
- Use
data-testidattributes in automation; they’re more stable than CSS selectors - Link every case to a requirement: no orphan tests
- Keep test data in one shared table, not hardcoded step by step
- Review test cases like code; peer review catches missing steps and vague expectations
- Version cases alongside requirements; when the requirement changes, the case should change in the same commit
Common Mistakes
- Vague expected results (“page loads correctly” — correct how, exactly?)
- Missing preconditions: “assumes user is logged in” is not a precondition; write the state explicitly
- Happy path only: the negatives are where the bugs hide
- Test data hardcoded in steps: one data table, or you’ll edit fifteen cases when the password rotates
- No traceability to requirements: can’t prove coverage in an audit, can’t find gaps yourself
- Script-level detail in automated cases: the code is the documentation; the case only needs intent and assertions
- Never updating cases: a stale test case wastes more time than no test case, because someone trusts it
- Copying the whole template for a trivial feature: keep the fields that earn their place; a login and a color picker don’t deserve the same depth
See Also
- ISTQB Glossary — canonical definitions for test design terms
- Playwright docs: writing tests — for the automation notes above
- Test strategy document template — scope, environments, and entry/exit criteria above individual cases
- Regression test checklist — for the suite these cases feed into
Frequently Asked Questions
How detailed should test steps be?
Detailed enough that someone who has never seen the feature can run the test and verify the result. One action, one expected result per row; if a step has two actions, split it. For automated tests the code is the documentation; steps are for manual execution.
Should I write test cases before or after development?
Before. Test cases come from requirements, not from the implementation. Writing them alongside acceptance criteria catches ambiguities while they're still cheap; "what should happen with a duplicate email?" is a requirements question, not a bug report.
How do I handle test cases that change frequently?
Parameterize. Keep the steps stable and put what varies in the data table; a login case with ten credential combinations in the test data section beats ten separate cases you'll forget to update.
What is the difference between a test case and a test scenario?
A scenario is the what ("verify login works"); a case is the how (eight steps with specific inputs and expected results). One scenario usually fans out into several cases: happy path, negatives, edge cases.
How many test cases per feature?
Match depth to risk. A simple feature needs five to ten cases (happy path, three to five negatives, a couple of edge cases); payment or auth flows can justify fifty or more. Don't write cases for trivial UI changes; review time is finite.
Related Resources
Test Strategy Document Template
Define your testing approach with this test strategy document template. Covers scope, types, tools, entry/exit criteria, risks, and schedule.
DocTest Coverage Report Template
A template for reporting test coverage by module, feature, and critical path with trend analysis and gap identification.
DocBug Reproduction Steps Template
A template for writing minimal, reliable bug reproduction steps that help developers reproduce and fix issues quickly.
DocRegression Test Checklist
A checklist for verifying existing functionality after changes: pre-deploy checks, post-deploy smoke tests, and rollback verification.