A test suite that only checks whether things work when everything goes right is dangerously incomplete. Real users enter invalid data, click buttons twice, paste emojis into number fields, and find creative ways to break your application. To catch these defects, you need three categories of test cases: positive tests that confirm correct behaviour, negative tests that verify proper error handling, and boundary tests that probe the edges where valid and invalid values meet.
Three Categories Every Tester Must Master
Positive, negative and boundary test cases form a complete picture of a feature’s behaviour. Skipping any category leaves blind spots that defects exploit in production.
# Testing a password field with all three categories
# Requirement: password must be 8–64 characters, at least 1 uppercase,
# 1 lowercase, 1 digit, 1 special character (!@#$%^&*)
PASSWORD_RULES = {
"min_length": 8,
"max_length": 64,
"requires_uppercase": True,
"requires_lowercase": True,
"requires_digit": True,
"requires_special": True,
"special_chars": "!@#$%^&*",
}
# ── POSITIVE test cases (valid inputs → should succeed) ──
positive_cases = [
{"id": "TC-PWD-POS-01", "input": "Abcdef1!", "desc": "Minimum valid: exactly 8 chars, all rules met"},
{"id": "TC-PWD-POS-02", "input": "MyP@ssw0rd", "desc": "Typical valid: 10 chars, mixed characters"},
{"id": "TC-PWD-POS-03", "input": "A" * 63 + "1!a", "desc": "Near maximum: 66 chars — wait, over 64!"},
]
# Fix the mistake above:
positive_cases[2] = {
"id": "TC-PWD-POS-03",
"input": "A" * 58 + "bcd1!@", # exactly 64 characters
"desc": "Maximum valid: exactly 64 chars, all rules met",
}
# ── NEGATIVE test cases (invalid inputs → should show error) ──
negative_cases = [
{"id": "TC-PWD-NEG-01", "input": "", "expected_error": "Password is required"},
{"id": "TC-PWD-NEG-02", "input": "abc", "expected_error": "Password must be at least 8 characters"},
{"id": "TC-PWD-NEG-03", "input": "abcdefgh", "expected_error": "Must contain uppercase letter"},
{"id": "TC-PWD-NEG-04", "input": "ABCDEFGH", "expected_error": "Must contain lowercase letter"},
{"id": "TC-PWD-NEG-05", "input": "Abcdefgh", "expected_error": "Must contain a digit"},
{"id": "TC-PWD-NEG-06", "input": "Abcdefg1", "expected_error": "Must contain a special character"},
]
# ── BOUNDARY test cases (edges of valid range) ──
boundary_cases = [
{"id": "TC-PWD-BND-01", "input": "Abcde1!", "length": 7, "expected": "Fail — 1 below minimum"},
{"id": "TC-PWD-BND-02", "input": "Abcdef1!", "length": 8, "expected": "Pass — exactly at minimum"},
{"id": "TC-PWD-BND-03", "input": "Abcdefg1!", "length": 9, "expected": "Pass — 1 above minimum"},
{"id": "TC-PWD-BND-04", "input": "A"*59 + "bcd1!", "length": 63, "expected": "Pass — 1 below maximum"},
{"id": "TC-PWD-BND-05", "input": "A"*58 + "bcd1!@", "length": 64, "expected": "Pass — exactly at maximum"},
{"id": "TC-PWD-BND-06", "input": "A"*59 + "bcd1!@", "length": 65, "expected": "Fail — 1 above maximum"},
]
print("POSITIVE cases (valid → should succeed):")
for tc in positive_cases:
print(f" {tc['id']}: {tc['desc']}")
print("\nNEGATIVE cases (invalid → should show specific error):")
for tc in negative_cases:
print(f" {tc['id']}: input='{tc['input']}' → {tc['expected_error']}")
print("\nBOUNDARY cases (edges of valid range):")
for tc in boundary_cases:
print(f" {tc['id']}: length={tc['length']} → {tc['expected']}")
if len(password) > 8 when the requirement says “at least 8 characters” (which should be >=). This classic off-by-one error means the boundary value of exactly 8 is incorrectly rejected. By testing at the boundary (7, 8, 9) and at the upper boundary (63, 64, 65), you catch these subtle implementation mistakes that other test types miss.Common Mistakes
Mistake 1 — Only writing positive test cases
❌ Wrong: Writing 10 test cases that all use valid inputs and expecting full coverage.
✅ Correct: Aiming for a balanced mix — roughly 30% positive, 40% negative and 30% boundary test cases for input-driven features. The exact ratio depends on the feature’s risk profile.
Mistake 2 — Testing boundaries in the middle of the range instead of at the edges
❌ Wrong: For a field accepting 8–64 characters, testing with inputs of 10, 30 and 50 characters — all safely in the valid range.
✅ Correct: Testing at 7 (just below), 8 (at minimum), 9 (just above), 63 (just below max), 64 (at maximum), and 65 (just above max).