Testing strategies
Black box and white box testing, alpha, beta and acceptance testing, destructive testing, and a black box test plan that finds two bugs.
Do this lesson in the simulatorAt GCSE you chose normal, boundary and erroneous test data and wrote test plans (F6.3). A level adds the question of who tests, when, and with what knowledge of the code. A professional project uses several strategies, because each finds errors the others miss.
What testing is for
Testing is running the system on purpose to find errors, and to show that it meets its requirements. It looks for three kinds of error:
- syntax errors, which break the rules of the language and stop the program being translated;
- run-time errors, which crash the program while it runs, such as dividing by zero or reading past the end of a list;
- logic errors, where the program runs but gives the wrong result, such as
<=where<was meant.
Testing can show that errors are present. It can never prove there are none, since no set of tests tries every possible input. So test data is chosen to be the inputs most likely to reveal errors.
Test data at A level
| Kind | Meaning | For a gap of 10 cm to under 40 cm |
|---|---|---|
| Normal (typical) | ordinary values the system should accept and handle | 25 |
| Boundary (extreme) | values at the edges of a range, and just either side of each edge | 9, 10, 39, 40 |
| Erroneous | data that should be rejected: the wrong type, or impossible values | "far", -5 |
Boundaries matter because they are where < and <= get swapped. Any place where the expected result changes is a boundary, not just the ends of an accepted range: if speed changes at 40 cm, then 39 and 40 must both be tested.
Black box testing
In black box testing the tester does not look at the code. Tests are designed from the specification alone: for these inputs, the specification says this output. The system is a black box: inputs go in, outputs come out.
- It tests what the user actually cares about: does it do what was asked?
- The tester needs no programming knowledge, and is not misled by assumptions in the code.
- It cannot tell whether every part of the code has been run, so some paths may never be tested.
A useful technique is to divide the inputs into partitions that the specification treats the same way, then test one normal value in each partition and the boundaries between them.
White box testing
In white box (or glass box) testing the tester uses the code to design tests, aiming to run every statement, every branch of every if, and every loop zero, one and many times. It is usually done by developers, often with a trace table or the debugger.
# which branches have the tests run? record each one as it happens
ran = set()
def speed_for(gap):
if gap < 10:
ran.add("stop branch")
return 0
elif gap < 40:
ran.add("slow branch")
return 30
else:
ran.add("fast branch")
return 60
for gap in [25, 30, 35]: # three normal tests, all in the same partition
speed_for(gap)
print("branches run:", sorted(ran))
print("coverage:", len(ran), "of 3")
Three tests, but only one branch run: 2 of the 3 branches are completely untested. White box testing finds this; black box testing with careless data would not.
- It finds untested paths and dead code.
- It needs someone who can read the code, and it only tests the code that exists: it cannot notice a feature that was never written.
The two are complementary: black box checks the program does what the specification says, white box checks every part of the code has been exercised.
Who tests, and when
| Strategy | Who | When | Finds |
|---|---|---|---|
| Unit testing | developers | as each module is written | errors inside one module |
| Integration testing | developers | as modules are combined | errors in how modules work together |
| System testing | the developer's testing team | when the whole system is built | failures against the specification |
| Alpha testing | in-house staff who did not write it | before release, on an unfinished version | bugs, before any outsider sees them |
| Beta testing | a limited group of real users outside the company | near release | problems in real environments, on hardware and in situations the developers did not think of |
| Acceptance testing | the client or end users | on delivery | whether the system meets the requirements, so the client accepts it |
Acceptance testing is often in the contract: the client checks the system against the agreed success criteria, and only then signs it off and pays.
Regression testing re-runs the old tests after every change, to catch a fix that breaks something that used to work. It is why automated tests are so valuable: re-running a hundred tests by hand after every change is impractical.
Destructive testing
Destructive testing deliberately tries to break the system: impossible inputs, huge amounts of data, many users at once, the power cut mid-save, the network lost. The aim is to find out how it fails, and make sure it fails safely. For a robot: what happens if the distance sensor reads nothing, the battery dies mid-corridor, or a student stands in front of it and does not move? A robot that stops and signals for help has passed; one that pushes on has not.
Destructive tests on hardware may damage the device, which is why they are done on test units and not on the ones sold.
A test plan
A test plan is written in design, before the code, and filled in as tests are run:
| No. | Purpose | Data | Kind | Expected | Actual | Pass |
|---|---|---|---|---|---|---|
| 1 | medium gap gives slow speed | 25 | normal | 30 | ||
| 2 | lowest gap for slow speed | 10 | boundary | 30 | ||
| 3 | just below the slow range | 9 | boundary | 0 | ||
| 4 | text is rejected | "far" |
erroneous | error |
The expected result comes from the specification. Filling it in after running the test turns testing into describing whatever the program happened to do.
In the NEA you test twice: iterative testing as each part is built (with evidence such as screenshots or output, and the fixes you made), and post-development or final testing of the finished program against every success criterion, ideally including feedback from real users.
Task: test the black box
A teammate has written speed_for(gap). Its specification is:
gapis a distance in cm, and should be anintor afloat.- If
gapis not a number, or is less than 0, it must raiseValueError. - Otherwise it returns
0ifgapis less than 10,30ifgapis from 10 up to but not including 40, and60ifgapis 40 or more.
Do not change speed_for: your job is to test it from the specification. Make a list tests of tuples (value, expected, kind), where expected is the number the specification says, or the string "error" if it should raise ValueError, and kind is "normal", "boundary" or "erroneous". Include at least two normal tests, the four boundary values 9, 10, 39 and 40, and the erroneous values "far" and -5.
Run each test, treating a ValueError as the result "error". For each, print <value!r> <kind>: pass if the result matches, or <value!r> <kind>: FAIL (got <result>) if it does not, where <value!r> is the value as repr shows it (so the string appears as 'far'). At the end print failed: <n>. The robot stays still.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def speed_for(gap):
if not isinstance(gap, (int, float)):
raise ValueError("gap must be a number")
if gap < 10:
return 0
elif gap <= 40:
return 30
else:
return 60
tests = []
Challenges
- Your tests found two bugs. Fix
speed_for, run the tests again, and check they all pass. What is this kind of re-testing called? - Write the extra white box tests needed to run every branch of the fixed function, including the one that raises the error.
- Plan three destructive tests for the delivery robot, and say what a safe failure looks like for each.