Boundary Value Analysis & Equivalence Partitioning
Most bugs hide at boundaries and in the gaps between valid and invalid inputs. Boundary value analysis and equivalence partitioning are two complementary techniques that systematically target these areas, letting you write fewer tests that cover more of the real risk.
Equivalence Partitioning
Equivalence partitioning divides the input space into groups (partitions) where the system is expected to behave the same way for any value in the group. Instead of testing every possible input — which is infinite for most fields — you test one representative from each partition.
The Logic
If the system handles age = 25 correctly, it almost certainly handles age = 26 the same way. Testing both doesn't reveal more bugs than testing either one. Testing both is redundant. Testing only one, plus one from each other partition, gives the same coverage with fewer tests.
The partitions for a field are defined by the system's behavior rules:
- Valid values → the system should accept and process
- Invalid values → the system should reject with an appropriate error
Example: Age Field (Must be 18–65)
Partitions:
- Valid: 18–65 (system processes the input)
- Invalid — too low: below 18 (system rejects with "must be at least 18")
- Invalid — too high: above 65 (system rejects with "maximum age is 65")
Representatives:
- Valid: 35 (any value in 18–65)
- Too low: 5 (any value below 18)
- Too high: 80 (any value above 65)
Three tests instead of the infinite space of possible age inputs.
Multiple Input Dimensions
For multiple inputs, partition each independently and test each partition:
Discount calculation:
- Customer tier: "premium", "standard", "trial" (three partitions)
- Purchase amount: <$50, $50–$199, ≥$200 (three partitions)
Minimum tests: 6 (one for each partition, not 3×3=9 combinations, unless combinations matter).
If combinations matter (premium customers get a different threshold for the $200 tier bonus), use decision table testing.
Partitioning Non-Numeric Input
String fields (email address):
- Valid format:
user@example.com - Missing @:
userexample.com - Missing domain:
user@ - Empty string: ``
- Too long: 300+ characters
Enumeration (status field):
- Valid values:
active,inactive,suspended - Invalid string:
deleted,unknown - Null/empty
File upload:
- Valid: PNG under 5MB
- Wrong type: PDF, EXE
- Too large: 10MB PNG
- Zero bytes: empty file
Boundary Value Analysis
Boundary value analysis (BVA) extends equivalence partitioning by focusing on the values at the edges of partitions — exactly where most bugs occur.
The insight: off-by-one errors are the most common source of boundary bugs. Programmers frequently write > when they mean >=, or use the wrong boundary in a comparison. Testing at and adjacent to boundaries catches these.
Standard BVA (2-value)
For each boundary, test the value just inside and just outside:
Age field (18–65):
- Lower boundary at 18: test 17 (invalid) and 18 (valid)
- Upper boundary at 65: test 65 (valid) and 66 (invalid)
4 tests catch the most common boundary bugs.
3-Value BVA
For each boundary, test just below, at, and just above:
- 17, 18, 19 (lower boundary)
- 64, 65, 66 (upper boundary)
6 tests. More thorough, catches bugs in both directions from each boundary.
Determining "Adjacent" Values
For integers, "just below/above" is -1/+1. For decimals, it depends on the precision:
- Integer age: test 17, 18, 19
- Price with cents: test 49.99, 50.00, 50.01
- Inclusive vs exclusive boundaries matter: test the exact boundary value and one step in each direction
BVA in Practice
class TestAgeValidation:
# Valid partition representative
def test_valid_age(self):
assert validate_age(35) == True
# Lower boundary
def test_minimum_age_boundary_invalid(self):
assert validate_age(17) == False
def test_minimum_age_boundary_valid(self):
assert validate_age(18) == True
def test_one_above_minimum(self):
assert validate_age(19) == True
# Upper boundary
def test_one_below_maximum(self):
assert validate_age(64) == True
def test_maximum_age_boundary_valid(self):
assert validate_age(65) == True
def test_maximum_age_boundary_invalid(self):
assert validate_age(66) == False
# Edge cases
def test_zero(self):
assert validate_age(0) == False
def test_negative(self):
assert validate_age(-1) == FalseBoundary Value Analysis for Strings
Length limits (username: 3–20 characters):
| Value | Length | Expected |
|---|---|---|
| "ab" | 2 | Reject |
| "abc" | 3 | Accept |
| "abcd" | 4 | Accept |
| "a" × 19 | 19 | Accept |
| "a" × 20 | 20 | Accept |
| "a" × 21 | 21 | Reject |
Content that hits common parsing boundaries:
For a field that accepts CSV: test inputs with 0 commas, 1 comma, many commas, commas at start/end.
For a JSON field: test empty object {}, minimum valid object, maximum size object.
Combining EP and BVA
Equivalence partitioning identifies the partitions. BVA applies at each partition boundary. Used together:
- Define partitions using equivalence partitioning
- Identify boundaries between partitions
- Apply BVA at each boundary
- Add representative(s) from non-boundary areas of each partition
For a discount tier system (0–49: no discount, 50–99: 10%, 100+: 20%):
Partitions:
- No discount: 0–49
- 10% discount: 50–99
- 20% discount: 100+
Boundaries:
- Between no-discount and 10%: at 49/50
- Between 10% and 20%: at 99/100
Test cases:
- 25 (representative, no discount)
- 49 (upper boundary, no discount)
- 50 (lower boundary, 10%)
- 75 (representative, 10%)
- 99 (upper boundary, 10%)
- 100 (lower boundary, 20%)
- 150 (representative, 20%)
- 0 (edge: minimum possible)
- Negative (edge: invalid input)
9 carefully chosen test cases that cover the entire input space more thoroughly than random selection of 50 test cases.
Special Values
Beyond EP and BVA, certain values tend to reveal bugs regardless of the formal analysis:
Numeric:
- 0 (often treated specially)
- 1 (minimal non-empty)
- Maximum integer (
MAX_INT,MAX_LONG) - Minimum integer (
MIN_INT, negative max) - Floating point edge cases: 0.1 + 0.2 ≠ 0.3 in most languages
Strings:
- Empty string
"" - Single character
"a" - Spaces only
" " - Unicode:
"café","日本語", emoji"😀" - Special characters:
"<script>alert('xss')</script>,"; DROP TABLE users;--" - Null
Collections:
- Empty collection
- Single element
- Maximum allowed elements
- Duplicate elements
Dates:
- January 1 (year start)
- December 31 (year end)
- Leap day: February 29
- Non-leap year February 29 (invalid)
- Time zone transitions
- Daylight saving time boundaries
Applying EP and BVA to Real Features
Search Feature
Query length: Empty, 1 char, 2 chars (minimum), 3 chars, 500 chars (maximum), 501 chars (over limit)
Results page: 0 results, 1 result, page size - 1, page size, page size + 1, last page with partial results
Filters: No filters, all filters enabled, incompatible filter combinations
File Upload
Size: 0 bytes, 1 byte, max size - 1, max size, max size + 1
Type: Each valid type (test representative per type), invalid extension, correct extension with wrong content (zip disguised as jpg)
API Rate Limiting
Requests: 0, 1, rate limit - 1, rate limit, rate limit + 1
Time window: Request at start of window, end of window, boundary reset
Automating Boundary Testing
Property-based testing frameworks generate boundary cases automatically:
Python (Hypothesis):
from hypothesis import given, strategies as st
@given(st.integers(min_value=0, max_value=200))
def test_age_validation_property(age):
result = validate_age(age)
if 18 <= age <= 65:
assert result == True
else:
assert result == FalseHypothesis generates values including edge cases and shrinks failing examples to the minimal reproduction.
JavaScript (fast-check):
import fc from 'fast-check';
test('discount calculation covers full range', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 10000 }),
fc.constantFrom('premium', 'standard', 'trial'),
(amount, tier) => {
const discount = calculateDiscount(amount, tier);
expect(discount).toBeGreaterThanOrEqual(0);
expect(discount).toBeLessThanOrEqual(amount);
}
)
);
});Property-based testing doesn't replace manual EP and BVA analysis — it complements it. Manual analysis identifies the interesting partitions and boundaries; property-based testing explores within and around those areas at scale.
Continuous Testing with HelpMeTest
Boundary conditions in forms and APIs can regress when validation logic changes. A field that correctly rejected empty strings can start accepting them after a refactor. HelpMeTest tests that verify boundary behavior — "empty username is rejected," "username longer than 20 characters is rejected" — run continuously against the live system, catching these regressions before users report them.
The boundary tests you identify through EP and BVA analysis are exactly the kind of tests worth running continuously: they're specific, deterministic, and target the areas most likely to regress.