EthicsComputerCourses
Join training +91 90532 10052

Test Scenarios & Test Cases

Software Testing · Lesson 6 of 30 Beginner 6 min read Updated

A tester's most common written output is the test case: a precise recipe that anyone on the team can follow to check one behaviour and decide pass or fail. Before writing cases, testers list test scenarios, the situations worth testing. Good scenarios make sure nothing important is forgotten; good test cases make the results repeatable.

In this lesson you will learn the difference between scenarios, conditions and cases, the fields of a professional test case, how to write positive and negative cases, and how to turn a table of test cases into an automated, data-driven test.

From scenario to test case

These terms go from broad to detailed:

TermMeaningExample for a library app
Test scenarioA situation or user goal worth testing"Member logs in"
Test conditionA specific aspect of the scenario to check"Login with an email in capital letters"
Test caseExact preconditions, data, steps and expected resultTC-LOGIN-02 below
Test procedure (script)Several test cases in the order they should be run"Login, search, borrow, logout"
Test suiteA group of test cases run together"Login suite", "Regression suite"

A single scenario usually produces many test cases. For "member logs in" you would check a valid login, a wrong password, an empty field, an unknown email, a locked account, the "remember me" box and more.

Anatomy of a test case

Most teams and test management tools (TestRail, Zephyr, Xray, Azure Test Plans, or a shared spreadsheet) use similar fields:

  • ID: a unique, stable name such as TC-LOGIN-02.
  • Title: one line that says what is checked.
  • Requirement: the requirement or user story it covers, for traceability.
  • Preconditions: what must be true before you start.
  • Test data: the exact values to use.
  • Steps: numbered actions, one action per step.
  • Expected result: what must happen, specific enough to judge pass or fail.
  • Actual result and status: filled in when the test is run (Pass, Fail, Blocked, Not run).
  • Priority: how important the case is when time is short.

Here is one complete case for the Rohtak City Library app, stored as JSON the way many tools export it:

JSON
{
  "id": "TC-LOGIN-02",
  "title": "Login accepts an email typed in capital letters",
  "requirement": "REQ-AUTH-1",
  "priority": "High",
  "preconditions": "Member kabir@example.com exists and is not locked",
  "test_data": {"email": "KABIR@example.com", "password": "Lib@2026"},
  "steps": [
    "Open the login page",
    "Enter the email and password from the test data",
    "Click the Log in button"
  ],
  "expected": "Dashboard opens and shows 'Hello, Kabir'"
}

Notice the expected result: not "login works", but exactly what the tester should see.

Positive and negative test cases

Positive test cases use valid input and check that the system does what it should. Negative test cases use invalid or unexpected input and check that the system refuses it politely and safely. Beginners write mostly positive cases, but many real bugs hide in the negative ones: empty fields, wrong formats, extra spaces, very long text, a double click on "Pay".

Test cases as data

When many cases follow the same steps with different data, you can store them as a table and let code run them. This is data-driven testing. Run this example and look carefully at the second case:

PythonRun it yourself
USERS = {"kabir@example.com": "Lib@2026"}

def login(email, password):
    email = email.strip()
    if not email or not password:
        return "Email and password are required"
    if USERS.get(email) == password:
        return "Welcome"
    return "Invalid email or password"

test_cases = [
    {"id": "TC-LOGIN-01", "email": "kabir@example.com", "password": "Lib@2026", "expected": "Welcome"},
    {"id": "TC-LOGIN-02", "email": "KABIR@example.com", "password": "Lib@2026", "expected": "Welcome"},
    {"id": "TC-LOGIN-03", "email": "kabir@example.com", "password": "lib@2026", "expected": "Invalid email or password"},
    {"id": "TC-LOGIN-04", "email": "", "password": "Lib@2026", "expected": "Email and password are required"},
    {"id": "TC-LOGIN-05", "email": " kabir@example.com ", "password": "Lib@2026", "expected": "Welcome"},
]

print(f"{'ID':<12} {'Status':<6} Actual")
for case in test_cases:
    actual = login(case["email"], case["password"])
    status = "PASS" if actual == case["expected"] else "FAIL"
    print(f"{case['id']:<12} {status:<6} {actual}")
Output
ID           Status Actual
TC-LOGIN-01  PASS   Welcome
TC-LOGIN-02  FAIL   Invalid email or password
TC-LOGIN-03  PASS   Invalid email or password
TC-LOGIN-04  PASS   Email and password are required
TC-LOGIN-05  PASS   Welcome

The code trims spaces but forgets that email addresses are not case-sensitive, so TC-LOGIN-02 fails. Note that TC-LOGIN-03 is a negative case that should be refused: passwords are case-sensitive. Adding .lower() after .strip() fixes the defect. Frameworks such as pytest have this feature built in, called parametrisation, which you will meet in Testing with pytest.

Writing good test cases

A good test case is:

  • Single-purpose. It checks one behaviour, so a failure points to one problem.
  • Clear. A new team member can follow it without asking questions.
  • Specific. The expected result names exact values, messages or screens.
  • Independent. It does not depend on another test having run first; preconditions are stated.
  • Traceable. It links to a requirement or user story.
  • Reusable. Data is separate from steps where possible.

Teams often review test cases against such a checklist. This small script does a few of the checks automatically:

JavaScriptRun it yourself
const testCases = [
  { id: "TC-01", title: "Login with valid email and password", steps: 3, expected: "Dashboard opens and shows 'Hello, Kabir'" },
  { id: "TC-02", title: "Login with wrong password", steps: 3, expected: "Works fine" },
  { id: "TC-03", title: "Search and borrow a book", steps: 0, expected: "" },
  { id: "TC-04", title: "Forgot password sends OTP", steps: 4, expected: "OTP SMS arrives within 60 seconds" },
];

const vague = ["works", "fine", "correct", "properly", "ok"];

for (const tc of testCases) {
  const problems = [];
  const words = tc.expected.toLowerCase().split(/\W+/);
  if (tc.steps === 0) problems.push("no steps");
  if (!tc.expected) problems.push("no expected result");
  else if (vague.some((word) => words.includes(word))) problems.push("vague expected result");
  console.log(`${tc.id}: ${problems.length ? problems.join(", ") : "looks good"}`);
}
Output
TC-01: looks good
TC-02: vague expected result
TC-03: no steps, no expected result
TC-04: looks good

A script cannot judge everything. TC-03 also checks two things at once (search and borrow), which only a human reviewer will notice; it should be split into two cases.

Common mistakes

  • Vague expected results such as "should work" or "page is correct". Write the exact message, value or screen.
  • Steps that combine several actions. "Fill the form and submit" hides which action failed. One action per step.
  • Hidden dependencies. A case that only passes if another case ran first will fail randomly when the order changes.
  • Only positive cases. For every valid input, think of empty, wrong, too long, duplicate and badly formatted inputs.

Key points

  • A test scenario is a situation to test; a test case gives exact preconditions, data, steps and expected result.
  • Test cases have a unique ID, a requirement link and a priority so they can be traced and chosen when time is short.
  • Positive cases use valid input; negative cases check that invalid input is handled safely.
  • Data-driven testing runs the same steps with many rows of test data.
  • Good test cases are single-purpose, clear, specific, independent, traceable and reusable.

Exercise

Fix the login() function so TC-LOGIN-02 passes. Then add two more negative test cases to the table: one with an unknown email and one where the password field is empty. Run the program and make sure all seven cases pass.

Show solution
Python
USERS = {"kabir@example.com": "Lib@2026"}

def login(email, password):
    email = email.strip().lower()
    if not email or not password:
        return "Email and password are required"
    if USERS.get(email) == password:
        return "Welcome"
    return "Invalid email or password"

test_cases = [
    {"id": "TC-LOGIN-01", "email": "kabir@example.com", "password": "Lib@2026", "expected": "Welcome"},
    {"id": "TC-LOGIN-02", "email": "KABIR@example.com", "password": "Lib@2026", "expected": "Welcome"},
    {"id": "TC-LOGIN-03", "email": "kabir@example.com", "password": "lib@2026", "expected": "Invalid email or password"},
    {"id": "TC-LOGIN-04", "email": "", "password": "Lib@2026", "expected": "Email and password are required"},
    {"id": "TC-LOGIN-05", "email": " kabir@example.com ", "password": "Lib@2026", "expected": "Welcome"},
    {"id": "TC-LOGIN-06", "email": "rohan@example.com", "password": "Lib@2026", "expected": "Invalid email or password"},
    {"id": "TC-LOGIN-07", "email": "kabir@example.com", "password": "", "expected": "Email and password are required"},
]

for case in test_cases:
    actual = login(case["email"], case["password"])
    print(case["id"], "PASS" if actual == case["expected"] else "FAIL")
Output
TC-LOGIN-01 PASS
TC-LOGIN-02 PASS
TC-LOGIN-03 PASS
TC-LOGIN-04 PASS
TC-LOGIN-05 PASS
TC-LOGIN-06 PASS
TC-LOGIN-07 PASS

Note that the unknown email gets the same message as a wrong password. That is deliberate: telling an attacker which emails exist would be a security weakness.

Quick Quiz

1. Which is a test scenario rather than a test case?

2. Which expected result is written well?

3. What is a negative test case?

4. Running the same steps with many rows of input values is called:

Learn Software Testing with a mentor

Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call