Black-box and Glass-box Testing
Applied class for 2026-04-16-solid-principles-ii (Week 7). Applies the black-box/white-box testing concepts from java-testing (Week 5) to a worked example. All of these tasks ask for a set of inputs achieving some coverage goal — many different answers are equally correct, so answers here are reveal-only worked examples rather than checkable exercises.
The Bus class
public class Bus {
public Bus(int capacity) {...}
public int getCurrent() {...}
public int getAverageCount() {...}
public void stop(int on, int off) {...}
}Task 0 — write a smoke test
A smoke test just exercises normal/expected operation:
Bus bus = new Bus(10);
bus.stop(5, 0);
bus.stop(3, 1);
assertEquals(7, bus.getCurrent());
assertEquals(6, bus.getAverageCount());Task 1 — develop boundary scenarios (with justification)
| Scenario | Justification |
|---|---|
| Construct a bus with capacity 0 | A bus that can have no passengers is an uncommon scenario we may expect bugs from. |
| Construct a bus with a negative capacity | Unclear how this should behave — worth clarifying and testing. |
| Ask for the average after no stops | Averages usually involve division by count; want to confirm no exception when count is 0. |
Negative values for on/off |
Unclear behaviour — does a negative on remove passengers? |
| More passengers get off than are currently on the bus | Results in undefined behaviour. |
| More passengers on the bus than capacity | Behaviour isn’t specified, so worth observing. |
Task 2 — how can we make Bus’s behaviour fully specified?
Most of the boundary scenarios above come from unspecified/unclear behaviour. If we declare getCurrent() <= capacity an invariant of Bus (see java-specification), we must protect it everywhere:
- Constructor throws
IllegalArgumentExceptionifcapacity < 0. stopgets a precondition that bothonandoffare>= 0.stopthrowsIllegalStateExceptionifgetCurrent() + on - offwould be negative or exceedcapacity.
Glass-box testing: code coverage levels
See java-testing for the three levels (statement, branch, path). Consider:
public static int countOccurrences(int[] array, int target) {
int count = 0;
for (int num : array) {
if (num == target) {
count++;
}
}
return count;
}A single array containing the target at least once (e.g. [2, 3, 2], target 2) gives statement coverage (the if body runs); adding an array where the target never appears (e.g. [3], target 2) gives branch coverage too.
Worked example: ascending
/**
* @require numbers != null
* @ensure \result is true iff for all indices j such that
* 0 <= j < numbers.length - 1, numbers[j] <= numbers[j + 1]
*/
public boolean ascending(int[] numbers) {
boolean result = true;
for (int i = 0; i < numbers.length - 1; i++) {
if (numbers[i] > numbers[i + 1]) {
result = false;
}
}
return result;
}Statement coverage: [2, 1] (loop runs once, if body executes).
Branch coverage: [2, 1, 2], or the pair [2, 1] and [1, 2] (need the if to be both true and false).
Path coverage (0, 1, and 2-iteration cases, crossed with the if outcome each time):
| Input | Justification |
|---|---|
[] or [3] |
0 times through loop |
[3, 4] |
1 time through loop, if false |
[4, 3] |
1 time through loop, if true |
[3, 4, 5] |
2 times through loop, false, false |
[3, 4, 2] |
2 times through loop, false, true |
[4, 3, 5] |
2 times through loop, true, false |
[5, 4, 3] |
2 times through loop, true, true |
Worked example: maximum (bonus)
/**
* @require numbers != null
* @ensure numbers.length > 0 ==> (
* (\forall int i; 0 <= i < numbers.length ==> \result >= numbers[i]) &&
* (\exists int i; 0 <= i < numbers.length && \result == numbers[i]))
* @ensure numbers.length == 0 ==> \result = Integer.MIN_VALUE
*/
public int maximum(int[] numbers) {
int currentMaximum = Integer.MIN_VALUE;
for (int number : numbers) {
if (number > currentMaximum) {
currentMaximum = number;
}
}
return currentMaximum;
}Statement coverage: [1].
Branch coverage: [2, 1] (first element sets a new max, second doesn’t).
Path coverage:
| Input | Justification |
|---|---|
[] |
0 times through loop |
[Integer.MIN_VALUE] |
1 time through loop, if false |
[1] |
1 time through loop, if true |
[Integer.MIN_VALUE, Integer.MIN_VALUE] |
2 times through loop, false, false |
[Integer.MIN_VALUE, 1] |
2 times through loop, false, true |
[2, 1] |
2 times through loop, true, false |
[1, 2] |
2 times through loop, true, true |
Worked example: indexOf (bonus)
/**
* @require numbers != null
* @ensure numbers[\result] = number ||
* (\result == -1 &&
* \forall int j; 0 <= j < numbers.length ==> numbers[j] != number)
*/
public int indexOf(int[] numbers, int number) {
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == number) {
return i;
}
}
return -1;
}Statement coverage: indexOf([], 1) and indexOf([1], 1).
Branch coverage: indexOf([], 1) and indexOf([2, 1], 1).
Path coverage — note that with an early return inside the loop, “2 iterations, if true on the first” is unreachable (the method returns immediately), so two of the theoretically-possible paths for a 2-element input can’t actually be tested:
| Input | Justification |
|---|---|
indexOf([], 42) |
0 times through loop |
indexOf([1], 2) |
1 time through loop, if false |
indexOf([1], 1) |
1 time through loop, if true |
indexOf([1, 2], 3) |
2 times through loop, false, false |
indexOf([1, 2], 2) |
2 times through loop, false, true |
| Cannot be tested | 2 times through loop, true, false |
| Cannot be tested | 2 times through loop, true, true |