Testing and Debugging
Every exam board examines testing. You need to distinguish iterative from terminal testing, write a test plan that uses normal, boundary and erroneous data, trace through code to find logic errors and identify the three types of programming error.
In 1996 the Ariane 5 rocket exploded 37 seconds after launch. The cause was a 64-bit number being forced into a 16-bit field, which had never been tested with realistic flight data. The same code had worked fine on the smaller Ariane 4. Cost of the bug: about half a billion dollars in lost rocket and payload. Testing is not optional. It is what stops bad assumptions reaching production.
1. The three types of programming error
| Error type | When detected | Example | Caught by |
|---|---|---|---|
| Syntax | Before the program runs | pint("hi") - misspelled keyword; missing colon at end of if line | Compiler / interpreter; IDE warnings |
| Runtime | While the program runs | Dividing by zero; reading index 10 of a list of length 3; opening a missing file | Try/except; defensive validation |
| Logic | Never automatically | Calculating average by dividing by n+1; using < when you meant <= | Trace tables; testing; user reports |
If a question gives you faulty code and asks you to "identify and explain the error", look for one of these three. The wording in your answer should name the type ("logic error"), then say what the code does wrong, then say what it should do.
2. Iterative vs terminal testing
Iterative testing happens during development. Every time you finish a function or feature, you test it. If it fails you fix it immediately, while the code is fresh in your mind.
Terminal testing happens at the end on the whole assembled program against the original requirements. It is where you check that all the pieces still work together, that the user interface flows correctly, and that the program handles realistic full-scale data.
Both are required by the exam boards. Iterative testing catches bugs early when they are cheap to fix. Terminal testing catches the bugs that only appear when modules interact.
A bug introduced in week 1 and found in week 8 is far more expensive to fix than the same bug found in week 1: code on top of it has to be re-written, the developer has forgotten the context, and other modules may now depend on the wrong behaviour.
3. A proper test plan: normal, boundary, erroneous
For every input, your test plan must include all three kinds of data. Below is a test plan for a function that accepts ages between 0 and 130 inclusive.
Test | Input | Type | Expected output ------|-------|-------------|--------------------------- 1 | 14 | Normal | Accepted, store 14 2 | 65 | Normal | Accepted, store 65 3 | 0 | Boundary | Accepted (lower edge) 4 | 130 | Boundary | Accepted (upper edge) 5 | -1 | Erroneous | Rejected with error message 6 | 131 | Erroneous | Rejected with error message 7 | "abc" | Erroneous | Rejected with error message 8 | "" | Erroneous | Rejected with error message
If a question asks for "three test cases" you almost always need one of each kind: normal, boundary, erroneous. Picking three normal values is a wasted answer worth at most one mark.
# A simple validate-age function under test def validate_age(value): try: n = int(value) except ValueError: return False return 0 <= n <= 130 print(validate_age("14")) # True (normal) print(validate_age("0")) # True (boundary) print(validate_age("131")) # False (erroneous) print(validate_age("abc")) # False (erroneous)
bool ValidateAge(string value) { if (!int.TryParse(value, out int n)) return false; return n >= 0 && n <= 130; } Console.WriteLine(ValidateAge("14")); // True (normal) Console.WriteLine(ValidateAge("131")); // False (erroneous)
4. Trace tables for finding logic errors
A trace table records how each variable changes as the program runs. It is how you catch logic errors that exception handling cannot see.
total = 0 for i in range(1, 4): total = total + i print(total)
int total = 0; for (int i = 1; i < 4; i++) { total = total + i; } Console.WriteLine(total);
Line | i | total | Output ------------------|---|-------|------- total = 0 | - | 0 | i = 1, add | 1 | 1 | i = 2, add | 2 | 3 | i = 3, add | 3 | 6 | print(total) | 3 | 6 | 6
If the expected output was 10 (1+2+3+4), the trace table immediately shows the loop ended one iteration too early - a classic off-by-one bug fixed by changing range(1,4) to range(1,5).
5. A trace table for an if/else
Trace tables are not only for loops. Use them whenever the variables change in a way that is not obvious.
x = 6 y = 3 if x > y: z = x - y else: z = y - x print(z)
Line | x | y | z | Output --------------|---|---|---|------- x = 6 | 6 | - | - | y = 3 | 6 | 3 | - | x > y? Yes | 6 | 3 | - | z = x - y | 6 | 3 | 3 | print(z) | 6 | 3 | 3 | 3
Notice that the else branch never ran. Trace tables make conditional flow explicit, which is exactly what an examiner is testing in "complete the trace table" questions.
6. Practical debugging strategies
When a bug appears, don't guess. Use a sequence:
- Reproduce. Find the exact input that triggers it. A bug you cannot reproduce is a bug you cannot fix.
- Isolate. Comment out lines or add
printstatements to narrow down which line introduces the problem. - Trace. Walk through the suspect lines on paper or with a debugger, recording each variable like a trace table.
- Hypothesise. Form a single specific theory about what is wrong.
- Fix and re-test. Make the smallest change that should fix the theory, then re-run all tests, including ones that already passed.
A fix can re-introduce a bug that was already fixed. This is called a regression. Running the full test suite after every change catches regressions before they reach the user.
This catches out half the candidates. A program with a logic error runs fine and produces output - the output is just wrong. Exception handling does not catch logic errors. Only testing with known inputs and expected outputs does.
7. A six-mark question, fully marked
Question: A program calculates the percentage a student scored on a test out of 80. Write a test plan with three test cases (one of each type) and explain how each test would help find a different kind of bug. [6 marks]
- Normal: input 60. Expected output 75. (1 mark for valid normal value, 1 mark for sensible expected output.)
- Boundary: input 0 or 80. Expected output 0% or 100%. Catches off-by-one errors at the edge of the valid range.
- Erroneous: input -5, 81, or "abc". Expected output: rejected with an error message. Catches missing input validation.
Beyond the basics
1. Lost context. Two months after writing a function, the developer has forgotten how it works. Fixing it takes far longer than fixing it the same week.
2. Compound bugs. Other modules built on top of the broken one may now depend on the broken behaviour. Fixing the original bug breaks five other places.
3. No safety net during development. Without iterative tests, the developer has no way to know whether a change to function A silently broke function B until the terminal phase.
Cost grows because: a bug found at design time is just a sentence to rewrite; a bug found in coding is a function to rewrite; a bug found in terminal testing might mean rewriting how several modules talk to each other; a bug found after release damages user trust and may require an emergency patch released over the air. Every step roughly multiplies the cost.
ZeroDivisionError halfway through running. What kind of error is this?