Testing age=30 when a field accepts 18–120 tells you nothing about what happens at 17 or 18, which is where off-by-one bugs actually live. Equivalence partitioning groups all inputs by expected behavior so you pick one representative per group instead of repeating equivalent cases; boundary value analysis then targets the values at each partition edge, where >= 18 accidentally written as > 18 goes undetected. This guide covers both techniques with worked examples, how to combine them into a repeatable 10-minute test design process, and how to translate the results into parameterized Playwright tests.
What is equivalence partitioning
Equivalence partitioning (EP) splits all possible input values into groups (called partitions or equivalence classes) where every value in the group is expected to behave the same way. If the system handles one value from a partition correctly, it will handle all of them the same way. So you only need to test one representative from each partition.
Example: age field for a movie streaming service
Say the system has these rules:
- Users must be 18 or older to register
- Users under 13 are completely blocked
- Users 13–17 get a restricted "teen" account
- Users 18+ get a full account
- Age must be a whole number between 1 and 120
You can immediately identify partitions:
| Partition | Range | Type | Expected result |
|-----------|-------|------|----------------|
| Valid adult | 18–120 | Valid | Full account |
| Valid teen | 13–17 | Valid | Teen account |
| Under 13 | 1–12 | Invalid | Blocked |
| Zero or negative | ≤ 0 | Invalid | Error |
| Over max | > 120 | Invalid | Error |
| Non-integer | 17.5, "abc", "" | Invalid | Validation error |
From these partitions, you pick one representative from each. You don't need to test age=19, age=25, age=50, and age=100. They're all in the same partition. Test age=30 and you've covered all of them.
Your test cases become:
30→ Full account (valid adult partition)15→ Teen account (valid teen partition)10→ Blocked (under 13 partition)-1→ Error (zero/negative partition)200→ Error (over max partition)"seventeen"→ Validation error (non-integer partition)
6 tests instead of potentially hundreds. And you haven't lost coverage.
What is boundary value analysis
Boundary value analysis (BVA) is based on a well-observed fact: bugs tend to cluster at the edges of partitions, not in the middle.
Developers write code like if (age >= 18). The bug is almost never "works for 30, breaks for 31." The bug is almost always at the boundary: "works for 18, breaks for 17" or "meant to use >= but wrote >", so the cutoff is off by one.
BVA says: instead of just picking any representative from each partition, always test the values right at the boundary — the last valid value, the first valid value, and optionally the values just outside.
BVA values to test
For any boundary, you test:
- Last value before valid range (just below the minimum)
- First valid value (minimum)
- Last valid value (maximum)
- First value after valid range (just above the maximum)
For our age example with boundaries at 1, 12, 13, 17, 18, 120:
| Boundary | Values to test | Why |
|----------|---------------|-----|
| Minimum valid age (1) | 0, 1 | Off-by-one errors |
| Teen/blocked boundary (13) | 12, 13 | Correct partition assignment |
| Teen/adult boundary (18) | 17, 18 | The most common business rule bug |
| Maximum valid age (120) | 120, 121 | Upper limit enforcement |
Your BVA test cases:
0→ Error (below minimum)1→ Blocked (minimum valid input)12→ Blocked (last value in under-13 partition)13→ Teen account (first value in teen partition)17→ Teen account (last value in teen partition)18→ Full account (first value in adult partition)120→ Full account (last value in valid range)121→ Error (first value over maximum)
This is much more thorough than random testing and far more likely to catch real off-by-one errors.
EP and BVA work together
In practice, you use them together:
1. EP first: identify all the partitions (valid, invalid, edge cases)
2. BVA second: for any partition with a numeric or ordered range, test at the boundaries instead of (or in addition to) a random middle value
Here's what combined coverage looks like for the age field:
| Test | Input | Technique | Expected result |
|------|-------|-----------|----------------|
| 1 | -1 | BVA (below min) | Error |
| 2 | 0 | BVA (at absolute bottom) | Error |
| 3 | 1 | BVA (minimum valid) | Blocked |
| 4 | 12 | BVA (last blocked) | Blocked |
| 5 | 13 | BVA (first teen) | Teen account |
| 6 | 15 | EP (teen partition middle) | Teen account |
| 7 | 17 | BVA (last teen) | Teen account |
| 8 | 18 | BVA (first adult) | Full account |
| 9 | 30 | EP (adult partition middle) | Full account |
| 10 | 120 | BVA (last valid) | Full account |
| 11 | 121 | BVA (first over max) | Error |
| 12 | "abc" | EP (non-integer partition) | Validation error |
| 13 | "" | EP (empty partition) | Validation error |
13 test cases. These will catch nearly every real-world age validation bug, including the subtle ones.
Applying this to real features
Password length (8–64 characters)
Partitions:- Empty → error
- Too short (1–7) → error
- Valid (8–64) → accepted
- Too long (65+) → error
""(empty) → error7 chars→ error (last value below minimum)8 chars→ accepted (first valid value)64 chars→ accepted (last valid value)65 chars→ error (first over maximum)
20 chars → accepted
That's 6 tests covering the entire valid/invalid range.
Email field
This is a partitioning-only scenario (no clean numeric boundaries):
Partitions:- Empty → error
- Valid format (name@domain.com) → accepted
- Missing @ → error
- Missing domain → error
- Multiple @ signs → error
- International characters → depends on system spec
Test one value from each partition. Don't test 50 valid email formats — they all go in the "valid format" partition.
Dropdown with fixed values
If a field only accepts specific values (Small/Medium/Large), there are no boundaries to analyze with BVA. EP is enough:
- Valid value (Medium) → accepted
- Invalid value not in list (XL) → error
- Empty → error
Common mistakes to avoid
Testing too many values from the same partition
If you test ages 20, 25, 30, 35, and 45, you've written 5 tests that all live in the same partition. They'll either all pass or all fail together. You gained nothing with those 4 extra tests.
Fix: Pick one representative per partition, test at boundaries.Forgetting invalid partitions
Beginners often only think about valid input. Real bugs live in what happens with invalid input — negative numbers, empty strings, values that are slightly too large.
Fix: Always list invalid partitions explicitly. They're often where the interesting bugs hide.Missing non-obvious partitions
For a "discount code" field:
- Common partitions: valid code, invalid code, empty
- Easy-to-miss partitions: expired code, code already used, code for a different product, code with wrong case (is it case-sensitive?)
Applying BVA to non-ordered data
BVA only makes sense for data that has a natural ordering — numbers, dates, lengths. You can't apply BVA to a list of country codes or a set of enum values. Use EP for those.
A practical process
When you encounter any input field or business rule, run through this:
Step 1: What values does this field accept? What are the rules? Step 2: List all partitions — valid ones and invalid ones. Write out the expected behavior for each. Step 3: For any partition with a numeric or ordered range, identify the boundaries. List the last-invalid/first-valid and last-valid/first-invalid values. Step 4: Pick one representative from each non-boundary partition (for the middle of valid ranges). Add the boundary values. Step 5: Write your test cases, one per identified value.This takes 5–10 minutes per feature and gives you a defensible, efficient test set that you can explain to anyone.
Why this matters for QA automation
When you're writing Playwright tests, these techniques help you decide what to parameterize. Instead of writing 20 nearly-identical tests that just change the input value, you write one parameterized test with the 6–8 values that EP and BVA identified:
const AGE_CASES = [
{ input: '-1', expect: 'error', label: 'below minimum' },
{ input: '1', expect: 'blocked', label: 'minimum valid' },
{ input: '12', expect: 'blocked', label: 'last blocked' },
{ input: '13', expect: 'teen account', label: 'first teen' },
{ input: '17', expect: 'teen account', label: 'last teen' },
{ input: '18', expect: 'full account', label: 'first adult' },
{ input: '120', expect: 'full account', label: 'last valid' },
{ input: '121', expect: 'error', label: 'over maximum' },
];
for (const { input, expect, label } of AGE_CASES) {
test(`age ${label}: ${expect}`, async ({ page }) => {
await page.fill('[data-testid="age-input"]', input);
await page.click('[data-testid="submit"]');
await expect(page.locator('[data-testid="result"]')).toContainText(expect);
});
}Clean, readable, and each test has a clear reason to exist.
Key takeaways
- Equivalence partitioning groups inputs by expected behavior: test one value per group
- Boundary value analysis targets the edges between groups, where off-by-one bugs live
- Use EP to identify partitions, BVA to pick the exact values to test
- Always include invalid partitions: the "what happens with bad input" tests catch real bugs
- The goal is minimum tests, maximum coverage — not testing everything, but testing the right things
These two techniques are the foundation of structured test design. Once you internalize them, you'll find yourself applying them instinctively every time you look at a new feature, even before you open your test editor.
→ See also: Test Case Design Techniques: EP, BVA, Decision Tables, State Transition | How to Write a Test Case: Format, Examples, and Common Mistakes | Risk-Based Testing: Prioritizing What to Test When You Can't Test Everything