Boundary values and equivalence classes: how to apply them in practice
You cannot check every possible value of a field: there are far too many. Two techniques solve that together — equivalence classes cut the list down, boundary analysis says which of the remaining values to check first.
Equivalence classes: why one value is enough
A «quantity» field accepts numbers from 1 to 99. Trying 1, then 2, then 3 makes no sense: the service treats them identically. Every value the program handles the same way forms a class — and one representative of a class is enough.
There are usually three classes: valid values, values below the range and above it. On top of those come separate classes for a different data type — an empty field, a string instead of a number, a fraction instead of an integer, the field missing altogether. The last one gets forgotten often, and it behaves differently from an empty value.
The boundary: where things break most often
Bugs rarely live in the middle of a range. They live at the edge, because that is where the programmer chose between «greater than» and «greater than or equal» — and could be off by one. That is exactly what the mistake is called: an off-by-one error.
ISTQB distinguishes two-value and three-value BVA. Two-value BVA uses the boundary and its nearest value in the adjacent partition: 0 and 1, then 99 and 100. Three-value BVA adds the nearest value inside the valid partition: 2 and 98. It costs more but detects more off-by-one variants.
- 0 below the edge expect a refusal
- 1 the edge itself expect success
- 2 inside the range expect success
- 98 inside the range expect success
- 99 the edge itself expect success
- 100 above the edge expect a refusal
Where boundaries hide in an API
A number is the most obvious case but far from the only one. Nearly anything with a size, a length or a term has boundaries.
- Page size: per_page=0, 1, the maximum, the maximum + 1. A frequent find is a parameter silently ignored, with the service returning everything at once.
- Text length: an empty string, one character, the maximum from the requirements, the maximum + 1. And separately: spaces only — formally the string is not empty.
- Date ranges: start equal to end, start later than end, a range one day long, a date in the future.
- Money: zero, the smallest amount, one cent over the balance, a fractional part longer than two digits.
- How often an action is allowed: the last permitted request in the window and the one right after it — the one that should be refused.
What a boundary check looks like
The point is to change exactly one value and compare the answers. If the service answers the same way at the edge and beyond it, there is no check at all — and that is a defect regardless of how it looks on screen.
GET /products?per_page=50 → 200, 50 items in data GET /products?per_page=51 → expect 422 GET /products?per_page=0 → expect 422
The expected result for each line comes from the requirements, not from what you observed. If the requirements name no maximum, that is a finding in itself: with no upper bound a single request can ask for the whole catalogue at once.
What these techniques will not catch
Boundaries and classes work on one field at a time. Defects that come from a combination of fields — a discount plus a quantity plus what is left in stock — will not turn up this way: those need other techniques and plain attention to how values affect each other.
They also say nothing about access rights or about what happens when the same request is repeated. Those are separate kinds of check, and boundaries are only the place to start because they give the most findings for the least time.
Deriving tests from a requirement
Write partitions before choosing values. For “discount is an integer from 0 through 30”, partitions include a valid integer, below 0, above 30, decimal, string, null and missing. Boundaries apply only to ordered partitions; string and null are separate classes, not numerical neighbours.
-1 → below range, reject\n0 → lower boundary, accept\n1 → inside, accept (three-value BVA only)\n29 → inside, accept (three-value BVA only)\n30 → upper boundary, accept\n31 → above range, reject\n2.5, "10", null, missing → separate partitions
One representative is enough only when equal processing is a reasonable model. If zero has a special business meaning or 30 activates another promotion rule, those values belong to separate partitions.
Where the technique stops and what to add
Equivalence is a model, not a fact the system must obey. Revisit partitions after each finding: a defect often reveals a hidden boundary inside a presumed partition.
- Field combinations: use decision tables or pairwise when several conditions determine the result.
- Status changes: use state-transition testing instead of independent input values.
- Permissions and ownership: build an actor × action × object matrix.
- Dates and time: clarify timezone, inclusive boundaries and daylight-saving transitions.
- Money: clarify storage unit, rounding, currency and precision before selecting values.
Common boundary-analysis questions
Do I always need three values at a boundary?
No. Two-value BVA uses the boundary and the nearest value in the adjacent partition. Three-value BVA adds the value on the other side. Choose according to risk and desired coverage strength.
Are an empty string and a missing field one partition?
Not necessarily. Empty string is explicit, null may mean no value, and missing may trigger a default or required validation. Test them separately unless the contract proves identical handling.
Can BVA be applied to an enum?
An enum has no natural numerical order, so BVA usually does not apply. Test valid and unknown values, case handling and absence instead.
The technique does not stick on first reading. It sticks after you check the same boundary in a filter, in a page size and in a daily limit — and see that it is the same check every time.