Choosing the tiny sample of inputs most likely to find a defect.
π Where this lives: nearly every off-by-one bug you will ever meet lives at a boundary β the first element, the last element, the empty list, the maximum length, the value exactly at the limit. That is not a coincidence; it is a fact about how humans write conditions, and boundary value analysis exists to exploit it. A tester who tests only boundaries and nothing else will still find most of the defects, which tells you something important about where to spend limited effort. Search "off by one error boundary condition bug".
Black-box versus white-box
TWO FUNDAMENTAL APPROACHES, distinguished by what the tester
knows.
BLACK-BOX (FUNCTIONAL / BEHAVIOURAL) TESTING
Tests derived from the SPECIFICATION alone; the internal
structure is not considered. The tester asks: given this
input, is the output what the specification says?
β finds MISSING FUNCTIONALITY β code that was never written
cannot be covered by a structural test
β tests are valid across reimplementations
β can be written before the code exists
β cannot know which paths went untested; may duplicate effort
on inputs that take the same path
WHITE-BOX (GLASS-BOX / STRUCTURAL) TESTING
Tests derived from the CODE's internal structure. The tester
aims to exercise particular statements, branches or paths.
β can guarantee coverage of what IS there
β finds unreachable code, and logic that no specification
mentioned
β CANNOT FIND MISSING FUNCTIONALITY β the great blind spot
β tests break when the implementation is refactored
β 100% coverage does NOT mean correct: coverage measures
which lines RAN, not whether the assertions checked
anything meaningful
GREY-BOX testing uses partial knowledge of internals β for
example, knowing there is a cache, and therefore testing the
second identical request.
THE TWO ARE COMPLEMENTARY, NOT ALTERNATIVES. The standard
practice: design black-box tests from the specification, then
MEASURE coverage, and add white-box tests only for what the
black-box tests left unexercised. That ordering finds missing
functionality AND achieves coverage.
Black-box techniques
EQUIVALENCE PARTITIONING
Divide the input domain into classes of data from which test
cases can be derived, on the principle that all members of a
class should be treated identically by the program. Test ONE
value from each class β testing more adds cost without adding
information.
Partition both VALID and INVALID classes.
BOUNDARY VALUE ANALYSIS (BVA)
Errors cluster at the edges of equivalence classes, so test
AT and JUST OUTSIDE each boundary. For a range [a, b], test:
aβ1, a, a+1, bβ1, b, b+1
(some schemes use just aβ1, a, b, b+1)
Also test the boundaries of OUTPUT domains, and for structures,
the first and last elements and the empty and full cases.
WORKED β a licence fee validator.
SPECIFICATION: fee must be an integer in NPR from 500 to
10,000 inclusive. Applicant age must be 18 to 70 inclusive.
District code must be one of 77 valid two-digit codes.
EQUIVALENCE CLASSES for fee:
invalid low fee < 500
VALID 500 β€ fee β€ 10,000
invalid high fee > 10,000
invalid type non-integer, empty, negative, text
β 1 valid test + 4 invalid tests, from an infinite input
space.
BOUNDARY VALUES for fee: 499, 500, 501, 9999, 10000, 10001
BOUNDARY VALUES for age: 17, 18, 19, 69, 70, 71
COUNTING THE TESTS β this is the arithmetic examiners want:
exhaustive over fee alone (say 0..20000 integers)
= 20,001 tests
equivalence partitioning on fee = 5 tests
BVA on fee = 6 tests
EP + BVA on fee = 9 distinct
values
A reduction of over 2,000Γ with, in practice, most of the
defect-finding power retained. THAT RATIO IS THE WHOLE
POINT OF TEST DESIGN.
NOW THE COMBINATION PROBLEM:
fee 9 values Γ age 6 values Γ district 78 values
= 4,212 combinations
Too many. Two standard reductions:
EACH-CHOICE / ONE-FACTOR-AT-A-TIME: vary one parameter,
hold the others at a valid value
β 9 + 6 + 78 = 93 tests (minus overlap)
PAIRWISE (all-pairs) TESTING: every PAIR of parameter
values appears in at least one test. Empirically catches
the large majority of interaction defects, because most
defects involve one or two parameters, not three.
β for these three parameters, an all-pairs set is on the
order of 78 Γ 9 = 702 tests, driven by the largest two
domains
β and if district is itself partitioned to {valid, invalid}
rather than enumerated, pairwise drops to
9 Γ 6 = 54 tests. PARTITIONING FIRST, THEN COMBINING is
what makes combinatorial testing tractable.
OTHER BLACK-BOX TECHNIQUES:
DECISION TABLE TESTING enumerate condition combinations and
the required action; guarantees no
combination is forgotten (recall the
tabular specification from the
requirements topic)
STATE TRANSITION TESTING for stateful systems: test every
valid transition, and importantly
every INVALID one (what happens if you
approve an already-issued licence?)
USE-CASE TESTING tests derived from use-case main and
extension flows β good at finding
integration-level defects
ERROR GUESSING experience-driven: empty input, zero,
null, very long strings, special
characters, dates at year end,
leap days. Unstructured but
remarkably productive.
CAUSE-EFFECT GRAPHING a graphical technique linking input
conditions (causes) to actions
(effects), from which a decision
table is generated
White-box techniques and basis path testing
COVERAGE CRITERIA, in increasing strength β each subsumes the
one above:
STATEMENT COVERAGE every statement executed at least once
BRANCH / DECISION every branch of every decision taken
COVERAGE both ways
CONDITION COVERAGE every atomic condition evaluated both
true and false
CONDITION/DECISION both of the above
MODIFIED CONDITION/ every condition shown to independently
DECISION (MC/DC) affect the decision outcome β REQUIRED
by DO-178C for avionics software
PATH COVERAGE every independent path executed β
usually infeasible, because loops
generate unbounded path counts
WHY BRANCH COVERAGE BEATS STATEMENT COVERAGE β a one-line
demonstration:
if (x > 0) y = 1;
return y;
A single test with x = 5 executes BOTH statements β 100%
statement coverage. But the x β€ 0 branch was never taken,
and that is where `y` is uninitialised. 100% statement
coverage, defect missed. Branch coverage requires the second
test.
BASIS PATH TESTING (McCabe) β the standard white-box method:
1. draw the CONTROL FLOW GRAPH of the module
2. compute the CYCLOMATIC COMPLEXITY V(G) = E β N + 2
(equivalently, decision points + 1)
3. determine a BASIS SET of V(G) linearly independent paths
4. prepare a test case for each path
V(G) IS THE MINIMUM NUMBER OF TESTS for basis path coverage,
and an upper bound on the number needed for branch coverage.
WORKED β the fee validator, as code:
1 read(fee, age, district)
2 if (fee < 500 || fee > 10000)
3 return ERR_FEE
4 if (age < 18 || age > 70)
5 return ERR_AGE
6 if (!validDistrict(district))
7 return ERR_DISTRICT
8 return OK
DECISION POINTS: line 2 has 2 (the ||), line 4 has 2, line 6
has 1 β 5 decisions.
V(G) = 5 + 1 = 6
So SIX independent paths, and six test cases minimum:
# fee age district expected
βββββββββββββββββββββββββββββββββββββββββ
1 400 30 "01" ERR_FEE (fee < 500)
2 20000 30 "01" ERR_FEE (fee > 10000)
3 5000 17 "01" ERR_AGE (age < 18)
4 5000 71 "01" ERR_AGE (age > 70)
5 5000 30 "99" ERR_DISTRICT
6 5000 30 "01" OK
CHECK: does this set also achieve BOUNDARY coverage? NO β it
uses 400 and 20000, not 499/500 and 10000/10001. So the basis
set gives structural coverage and MISSES the off-by-one, which
is the likeliest actual defect (`<` written where `<=` was
meant).
THE COMBINED SET, replacing the arbitrary values with
boundaries:
fee = 499, 500, 10000, 10001
age = 17, 18, 70, 71
valid = 5000 / 30 / "01"
β 6 basis paths satisfied AND every boundary probed, in about
10 tests.
THE LESSON, and it is the most useful thing in this topic:
STRUCTURAL COVERAGE AND BOUNDARY COVERAGE ARE DIFFERENT
THINGS. You can have 100% branch coverage and still miss every
off-by-one, because a branch is covered by ANY value on each
side, and the defect lives at the specific value where the
comparison flips. Design tests from the specification's
boundaries; use coverage to check you missed nothing
structural.
The most valuable idea here: 100% branch coverage can coexist with every off-by-one defect intact. A branch is satisfied by any value on each side of the comparison, while the defect lives precisely at the value where < should have been <=. Coverage tells you what you failed to execute; it cannot tell you what you failed to check.
π Go further: the technique that answers "are my assertions actually checking anything?" is mutation testing. The tool deliberately introduces small faults into your code β flipping < to <=, replacing a return value with a constant, removing a statement β and re-runs your suite. If the tests still pass, that mutant "survived", meaning your suite cannot detect that class of defect. The mutation score is a far harsher and more honest metric than line coverage, and it is exactly the boundary problem above made mechanical. Search "mutation testing surviving mutants PIT".
π‘ Exam angle: distinguish black-box from white-box testing with the advantages of each, and state white-box's blind spot (it cannot find missing functionality). Be ready to apply equivalence partitioning and boundary value analysis to a stated range and list the test values β this is a near-certain question, so practise producing aβ1, a, a+1, bβ1, b, b+1. List the coverage criteria in increasing strength and show why branch coverage is stronger than statement coverage. Perform basis path testing: draw the flow graph, compute V(G) = E β N + 2, and tabulate that many test cases.