← back to the section

The requirement is a single line: "Age — from 18 to 60." There is nothing to check on the form and everything at once: 17, 0, −5, 150, "twenty-five", an empty field, a pasted paragraph. Whole numbers from 0 to 150 alone are 151 checks for one field, and there are eight fields.

Test design answers the question "what exactly do I take". Equivalence classes and boundary values cover most fields; where the thing to check is not a field but the order of actions, a third technique joins them — state transitions. We will work through all three on one order.

the "age" field accepts numbers from 18 to 60 one by one: 43 values, and nearly all say the same thing under 1818 to 60over 60 rejectedacceptedrejected 153070 one value per class — already 3 checks instead of 43 lower boundaryupper boundary 1718 6061 17rejected 18accepted 30accepted 60accepted 61rejected abcnot a number six checks instead of forty-three — both boundaries checked from both sides

Classes answer the question "which situations exist at all" and remove the repeats: to the program, 15, 16 and 17 are one and the same. Boundaries answer "where the situation changes" — and that is exactly where the comparison sits that people get wrong. 15 and 70 drop off the list: 17 and 61 do their job, because they live in the same classes and press against the edge as well.

Equivalence classes: forty-three checks become three

It feels like 19 and 37 are different data. Different for the form, not for the code: behind the field sits one condition, "not less than 18 and not more than 60 — accept, otherwise show an error", and 19 and 37 take the same branch of it: same lines of code, same row in the database, same answer. Twenty values on one branch are one check repeated twenty times.

That is what an equivalence class is: a group of values the program reacts to in the same way. If the reaction differs in any detail — 0 gets "please enter an age", −5 gets "age cannot be negative" — those are two classes.

"Age from 18 to 60" has three groups: under 18, 18 to 60, over 60. The neighbouring amount field, with thresholds at 3000 and 300,000, has four classes, and both middle ones are accepted yet behave differently.

Then come the classes the requirement never mentions: an empty field, letters instead of digits, a negative number, five thousand characters of text. They are called invalid, and checks built on them are negative ones: the requirement was not written for the person who pastes a string with a trailing space.

Boundaries: why bugs pile up at the edge of a class

Values inside a class are interchangeable, so it seems sensible to take something round from the middle — 30, 1500. That is how most bugs get missed: the middle of a class is the calmest place in the program, and the decision is made at the edge.

A boundary is written by a person, as a single comparison, and every comparison has a twin one character away: "greater than" and "greater than or equal to". Mix them up and the field accepts 17, or refuses 60. That is an off-by-one error, and a check on 30 will never see it. The second reason is the requirement itself: "free delivery from 3000" — is exactly 3000 already free? "From", "up to" and "over" are ambiguous, and an analyst and a developer read them differently.

Crossing the edge also changes more than the answer: a "delivery: free" line appears, the "Pay" button gives way to a message about the manager. Watch what changed next to it.

One below, exactly on, one above — and what counts as the step

The rule: for every boundary take a pair — the boundary value and its neighbour on the other side. "From 18 to 60" has two boundaries, so two pairs: 17 and 18, 60 and 61. A one-sided check catches half of the errors: you confirmed 18 is accepted and walked away, and "it accepts 17" stayed unseen. A third point (17, 18, 19) is added when the boundary is calculated: age from a date of birth, a limit from settings. For whole numbers the step is one, and beyond that it depends on what fills the field.

Money. The step is a cent: not 2999 and 3000, but 2999.99 and 3000.00. Whole units hide rounding bugs: 2999.995 rounds up to 3000 and wins free delivery. You also see what the field does with a comma instead of a dot.

Dates. The step is a day, or a second if the requirement mentions time: "the sale runs until 31 August inclusive" means it is on at 23:59:59 on 31 August and off at 00:00:00 on 1 September. With time zones, catch the boundary when it is tomorrow for the buyer and still today on the server.

Text length. The step is one character, and there are two boundaries: empty and one character at the bottom, exactly the limit and the limit plus one at the top. "Character" itself is arguable: an accented letter, an emoji and an ideograph count differently, and a field sold as "up to 100 characters" sometimes cuts the text at the ninetieth.

Take the wrong step and you have checked two random values near the boundary. A list has its own unit — items — and a file has bytes, while a toggle or a three-item dropdown has no boundaries at all: there are two or three cases.

Six checks instead of forty-three — as checklist lines

First map the field with classes, invalid ones included. Then take a pair from both sides of every boundary: 17, 18, 60, 61. Then add a representative where the boundaries did not reach: 30, "abc", empty.

InputExpectedWhy
17error "age from 18"one below the lower boundary
18the form submitsthe lower boundary, exactly
30the form submitsrepresentative of the class
60the form submitsthe upper boundary, exactly
61error "age up to 60"one above the upper boundary
emptyerror "please fill in the field"the "no value" class
abcletters cannot be typedthe "not a number" class

The word "error" in the middle column is not a result: it does not separate correct behaviour from "the form silently did not submit" — write the message text and where it appears, the bug report is assembled from the same line. The third column looks like overhead until the lower boundary is raised from 18 to 21: you see at once what to rewrite.

Splitting a field into classes, you will hit a place where the requirement says nothing — the same free delivery at exactly 3000. Do not guess: that is a finding for your questions about the requirements. And once the result depends on a combination of conditions — three conditions with three options make 27 combinations — you need decision tables and pairwise testing.

State transitions: check the order, not the value

Classes and boundaries take one field at one moment. An order has no "correct value" — it has a life. Payment works, "Cancel" works, and the bug sits in the order of things: an order that left with the courier an hour ago still gets cancelled.

A state is what has already happened to the order and what it is allowed to do now. The main path: new → paid → packed → shipped → delivered; two side ones sit next to it — cancelled, while the order has not left, and returned, once it has been delivered.

StateAllowedForbidden
newpay, cancelpack, ship
paidpack, cancel with a refundpay a second time
packedship, cancelpay
shippeddelivercancel, pay
deliveredopen a returncancel, deliver
cancellednothingeverything else

Every cell of the second column is a positive check: bring the order to that state, perform the action, confirm the state changed, the money was taken and the goods reserved. The third column's cells are negative ones: same start, but the system must refuse clearly and the state must stay put.

Bugs pile up in the third column because the ban is drawn on the screen rather than written in the code. The "Cancel" button is hidden after shipping, but the old screen is still open in another tab, and the request can be repeated directly. Same with a repeated transition: a double click on "Pay" takes the money twice. Whether the state really changed is visible in the database — in the status field.

In short

  • An equivalence class is a group of values with an identical reaction; differ in any detail and it is another class. There are always more classes than lines in the requirement.
  • Class boundaries are the most bug-prone spot: a hand-written comparison sits there, along with the ambiguous "from", "up to" and "over".
  • Every boundary takes a pair from both sides — 17 and 18, 60 and 61: a one-sided check catches half of the errors.
  • The step depends on the type of field: a cent for money, a day or a second for dates, a character for text.
  • The "age from 18 to 60" field comes out as six or seven checks instead of forty-three.
  • An object with a lifecycle is checked by its transitions: allowed ones are positive checks, forbidden ones negative. Bugs live on the forbidden ones: cancelling a shipped order, paying twice.