Lesson 12 of 13
Lesson 12

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.

Test data, test plans, trace tables, debugging
Language:

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.

Think about it: If your program asks for an age and stores it, list five different inputs that should make the program do something other than store the value as given.
Iterative testing
Testing that happens repeatedly while the program is being written. Each module is tested as it is built.
Terminal (final) testing
Testing carried out once at the end on the complete program before release.
Normal data
Sensible, expected input that should be accepted, e.g. age = 14.
Boundary data
Values right on the edge of what is allowed, e.g. age = 0 or age = 130. Where off-by-one errors hide.
Erroneous data
Input that should be rejected, e.g. age = -5, age = 200, age = "abc".
Trace table
A table showing how the value of each variable changes as the program runs, line by line.
Syntax error
A rule-breaking mistake. The program will not run at all because the compiler/interpreter rejects it.
Runtime error
An error that happens while the program is running, e.g. dividing by zero or opening a missing file.
Logic error
The program runs without crashing but produces the wrong result. The hardest to spot.
Debugging
The process of finding and fixing errors in code, often using trace tables, breakpoints and print statements.

1. The three types of programming error

Error typeWhen detectedExampleCaught by
SyntaxBefore the program runspint("hi") - misspelled keyword; missing colon at end of if lineCompiler / interpreter; IDE warnings
RuntimeWhile the program runsDividing by zero; reading index 10 of a list of length 3; opening a missing fileTry/except; defensive validation
LogicNever automaticallyCalculating average by dividing by n+1; using < when you meant <=Trace tables; testing; user reports
Examiner shorthand

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.

Why "test at the end" is too late

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
Examiner tip

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:

  1. Reproduce. Find the exact input that triggers it. A bug you cannot reproduce is a bug you cannot fix.
  2. Isolate. Comment out lines or add print statements to narrow down which line introduces the problem.
  3. Trace. Walk through the suspect lines on paper or with a debugger, recording each variable like a trace table.
  4. Hypothesise. Form a single specific theory about what is wrong.
  5. Fix and re-test. Make the smallest change that should fix the theory, then re-run all tests, including ones that already passed.
Why re-test passing tests?

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.

"If it runs without an error message, it works"

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]

Mark scheme - up to 2 marks per test, 6 total
  • 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

A junior developer says "we don't need iterative testing, we'll just do a big test phase at the end". Give three concrete reasons this approach fails in practice, and explain why the cost of a bug grows the longer it stays in the codebase.
Three reasons it fails:
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.
Q1. A program asks for a percentage between 0 and 100 inclusive. Which value is the best example of boundary data?
100 is right on the edge of what is allowed. 75 is normal; "hello" and -50 are erroneous.
Q2. Which type of error will a trace table help you find?
Trace tables expose how variables change line by line, which is exactly how you spot logic errors.
Q3. What is the main purpose of iterative testing?
Iterative testing happens during development so problems are caught and fixed at low cost.
Q4. A program crashes with the message ZeroDivisionError halfway through running. What kind of error is this?
The program ran (so syntax was fine) but failed during execution because of a divide-by-zero.
Q5. Which of the following is the best definition of regression testing?
Regression testing protects against fixes accidentally re-introducing old bugs.
Programming - Lesson 12
Testing and Debugging
Starter activity
Write a tiny program on the board: "Ask the user for an age, store it, print it back". Ask the class in pairs to brainstorm every input that could break or mislead this program. Collect responses on the board and group them into Normal, Boundary, Erroneous to introduce the three categories before naming them formally.
Lesson objectives
1
Distinguish syntax, runtime and logic errors with named examples of each.
2
Explain when iterative testing is used and when terminal testing is used.
3
Write a test plan that includes normal, boundary and erroneous data for any given input.
4
Complete a trace table for a short program containing a loop or conditional.
5
Describe a sensible debugging process: reproduce, isolate, trace, hypothesise, fix, re-test.
6
Justify why iterative testing reduces overall project cost.
Key vocabulary
iterativeterminalnormal databoundary dataerroneous datatrace tablesyntax errorruntime errorlogic errordebuggingregressiontest plan
Discussion questions
If logic errors cannot be caught by exception handling, what process actually finds them?
Why is "the program runs" not the same as "the program is correct"?
When would a developer deliberately add boundary tests for inputs that should be impossible?
Discuss a real-world software disaster (Therac-25, Ariane 5, NHS Spine, Boeing 737 MAX). What kind of testing would have caught it?
Exit tickets
Define normal, boundary and erroneous data. [3 marks]
A login form accepts 8-12 character passwords. Write three test cases, one of each type. [3 marks]
Complete a trace table for a short loop given by the teacher. [4 marks]
Explain why iterative testing reduces total project cost. [3 marks]
Homework suggestion
Take the validate-age function from the lesson. Extend it to accept ages 5-99 and reject anything else. Write a complete test plan covering normal, boundary and erroneous data with at least eight tests, run each one, record the actual outputs and identify any bugs you found. Hand in the test plan and the fixed code.
Classroom tools