Designs comprehensive test cases using systematic techniques (boundary value analysis, equivalence partitioning, mutation testing, property-based testing). Use when writing tests, improving test coverage, finding edge cases, or when user mentions test cases, testing, or test design.
Design comprehensive test cases that systematically find bugs using proven QA techniques.
Use this skill when writing new tests, improving test coverage, finding edge cases, designing test strategies, or reviewing test quality.
Supporting files: TECHNIQUES.md for detailed techniques, BUG-PATTERNS.md for common bug types, GO-TESTING.md for Go-specific patterns, HEURISTICS.md for testing mnemonics.
Apply systematic techniques to find bugs others miss:
func TestCalculateDiscount(t *testing.T) {
tests := map[string]struct {
price float64
quantity int
want float64
wantErr bool
}{
// Equivalence Partitions
"valid small order": {100, 5, 500, false},
"valid bulk order": {100, 100, 9000, false}, // 10% bulk discount
// Boundary Values
"quantity zero": {100, 0, 0, false},
"quantity one": {100, 1, 100, false},
"bulk threshold-1": {100, 49, 4900, false},
"bulk threshold": {100, 50, 4500, false}, // discount kicks in
"bulk threshold+1": {100, 51, 4590, false},
// Error Cases
"negative quantity": {100, -1, 0, true},
"negative price": {-100, 5, 0, true},
"zero price": {0, 5, 0, false},
// Edge Cases (Error Guessing)
"max int quantity": {1, math.MaxInt32, 0, true},
"float precision": {0.1, 3, 0.3, false},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// test implementation
})
}
}
Follow this systematic process:
Test Design Checklist:
- [ ] Step 1: Analyze requirements and identify test conditions
- [ ] Step 2: Apply specification-based techniques (EP, BVA, DT)
- [ ] Step 3: Apply structure-based techniques (coverage analysis)
- [ ] Step 4: Apply experience-based techniques (error guessing)
- [ ] Step 5: Prioritize using RCRCRC heuristic
- [ ] Step 6: Validate test quality (mutation testing)
Identify inputs, outputs, preconditions, and invariants:
| Aspect | Questions to Answer |
|---|---|
| Inputs | What are valid/invalid ranges? What types? |
| Outputs | What should success look like? What errors? |
| Preconditions | What state must exist before the operation? |
| Invariants | What must remain true throughout? |
| Side Effects | What state changes occur? |
Divide input domain into classes where behavior is equivalent:
Input: age (integer)
Partitions:
- Invalid: age < 0
- Minor: 0 <= age < 18
- Adult: 18 <= age < 65
- Senior: age >= 65
Test one value from each partition:
- age = -5 (invalid)
- age = 10 (minor)
- age = 30 (adult)
- age = 70 (senior)
Test at partition boundaries where bugs cluster:
For partition boundaries at 0, 18, 65:
Test: -1, 0, 1, 17, 18, 19, 64, 65, 66
For complex business logic with multiple conditions:
| Conditions | R1 | R2 | R3 | R4 |
|---|---|---|---|---|
| Premium member | Y | Y | N | N |
| Order > $100 | Y | N | Y | N |
| Actions | ||||
| Apply 20% off | X | |||
| Apply 10% off | X | X | ||
| No discount | X |
For stateful systems:
States: [Idle] -> [Processing] -> [Complete/Failed]
Test transitions:
1. Idle -> Processing (valid: submit order)
2. Processing -> Complete (valid: payment success)
3. Processing -> Failed (valid: payment declined)
4. Idle -> Complete (invalid: should fail)
Target coverage metrics:
| Level | Coverage Target | When to Use |
|---|---|---|
| Statement | 80%+ | Minimum for production code |
| Branch | 80%+ | Conditional logic |
| MC/DC | 100% | Safety-critical systems |
Branch coverage example:
func process(a, b bool) string {
if a && b { // Branch 1: both true
return "both"
} else if a { // Branch 2: only a
return "a"
} else { // Branch 3: neither/only b
return "other"
}
}
// Tests for 100% branch coverage:
// {true, true} -> "both" (Branch 1)
// {true, false} -> "a" (Branch 2)
// {false, true} -> "other" (Branch 3)
// {false, false} -> "other" (Branch 3, redundant)
Target common bug patterns:
| Category | Examples to Test |
|---|---|
| Off-by-one | Loop bounds, array indices, fencepost |
| Null/Empty | nil, "", [], {} |
| Numeric | 0, -1, MAX_INT, MIN_INT, NaN, Inf |
| Strings | Unicode, very long, special chars |
| Concurrency | Race conditions, deadlocks |
| Resources | Leaks, exhaustion, timeouts |
Prioritize test areas:
| Letter | Focus | What to Test |
|---|---|---|
| Recent | New/changed code | Features added in current sprint |
| Core | Critical functionality | Main business flows, payment |
| Risk | High-risk areas | Complex algorithms, security |
| Configuration | Environment-dependent | DB connections, API keys |
| Repaired | Recently fixed bugs | Regression tests for fixes |
| Chronic | Frequently failing | Areas with bug history |
Verify tests can detect code changes:
# Go mutation testing
go-mutesting ./...
# Interpret results:
# - Killed mutants: Tests detected the change (good)
# - Survived mutants: Tests missed the change (add tests)
# - Mutation score: killed / total (aim for >80%)
Test invariants across many random inputs:
func TestReverse_Involution(t *testing.T) {
// Property: reverse(reverse(s)) == s
rapid.Check(t, func(t *rapid.T) {
s := rapid.String().Draw(t, "s")
if reverse(reverse(s)) != s {
t.Fatalf("involution violated for %q", s)
}
})
}
tests := map[string]struct {
input InputType
want OutputType
wantErr bool
}{
"descriptive name": {input: x, want: y},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got, err := Function(tc.input)
if (err != nil) != tc.wantErr {
t.Errorf("error = %v, wantErr %v", err, tc.wantErr)
return
}
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
for name, tc := range tests {
tc := tc // capture for parallel
t.Run(name, func(t *testing.T) {
t.Parallel()
// test body
})
}
Before finalizing tests:
Test Quality Check:
- [ ] Each test has a clear, descriptive name
- [ ] Tests are independent (no shared mutable state)
- [ ] Error cases are covered
- [ ] Boundary values are tested
- [ ] Tests run quickly (<100ms each)
- [ ] No test smells (duplicate setup, magic numbers)
- [ ] Mutation score >80% for critical code
Complete technique reference: See TECHNIQUES.md Common bug patterns: See BUG-PATTERNS.md Go-specific testing: See GO-TESTING.md Testing heuristics: See HEURISTICS.md