Grade by Grade
A. Programming Concepts and Skills · Programming Basics: Data, Decisions and Loops

Tracing, Testing and Writing Clean Code

A4.4 A4.5 A4.2 A4.1About 75 minutes
Learning goals
  • I can trace a program with loops and selection using a trace table.
  • I can add temporary print statements to show the state of variables while debugging.
  • I can write a test plan with typical, boundary and invalid cases and expected results worked out in advance.
  • I can review code for professional conventions: names, constants, comments, indentation and layout.

Words to know

trace table
A table with a column for each variable (and the output) and a row for each step, filled in by hand to follow a program exactly.
test case
One set of inputs together with the output the program should produce.
test plan
A table of test cases with expected results, actual results and pass/fail, written before testing.
boundary case
A test value at the edge of a range or condition, or just on either side of it.
invalid case
Input the program should reject or handle safely, such as a negative age or text where a number is expected.
magic number
A number typed straight into code with no name to explain it. Replace it with a named constant.
PEP 8
Python's official style guide: 4-space indents, snake_case names, UPPER_CASE constants, spaces around operators, and more.

Proving your program works

"It worked when I ran it" is not proof. A program can work for the one input you tried and fail for many others. Professional developers trace code to understand it, test it with a planned set of cases, and keep it clean so others can check it. This lesson pulls together the code maintenance expectations (A4) you have been practising all unit.

Tracing with a trace table

You have traced small programs since Lesson 2. For loops and selection, make a table with:

  • one column for each variable,
  • a column for any condition being tested (True/False),
  • a column for output,
  • one row each time a value changes or a condition is tested.

Go slowly and do exactly what the computer would do, not what you think the code means. That is how tracing finds logic errors.

Worked example

Example 1: Trace a loop with a condition

Trace this code and give the output.

  • total = 0
  • for n in range(1, 7):
  •     if n % 3 != 0:
  •         total += n
  • print(total)
  1. Start: total = 0.
  2. n = 1: 1 % 3 = 1, condition True, total = 1.
  3. n = 2: True, total = 3.
  4. n = 3: 3 % 3 = 0, condition False, total stays 3.
  5. n = 4: True, total = 7. n = 5: True, total = 12.
  6. n = 6: False, total stays 12. The range ends (6 was the last value).

Output: 12 (1 + 2 + 4 + 5, skipping multiples of 3).

nn % 3 != 0total
(start)0
1True1
2True3
3False3
4True7
5True12
6False12
Example 1's trace table. Output after the loop: 12.

Debugging with print statements

For longer programs, let the computer trace for you: add temporary print statements that show the variables at key points. Label them so you can tell them apart from normal output, then remove them (or comment them out) when the bug is fixed. An IDE debugger (Lesson 14) does the same job without editing the code.

total = 0
for n in range(1, 7):
if n % 3 != 0:
total += n
print("DEBUG n =", n, "total =", total)
print(total)

The DEBUG lines print the program's own trace table. Delete them once the program works. Output, line by line: DEBUG n = 1 total = 1 | DEBUG n = 2 total = 3 | DEBUG n = 3 total = 3 | DEBUG n = 4 total = 7 | DEBUG n = 5 total = 12 | DEBUG n = 6 total = 12 | 12

Testing with a full range of cases

Expectation A4.5 asks you to validate programs with a full range of test cases. A good test plan includes:

  • Typical (normal) cases: ordinary values in each branch.
  • Boundary cases: values exactly at each edge of a condition, and just on each side (49, 50, 51 for >= 50).
  • Extreme cases: the smallest and largest allowed values, such as 0 and 100.
  • Invalid cases: values the program should reject: out of range, negative, text instead of a number.
  • For loops: inputs that make the loop run 0 times, once, and many times (for example, a sentinel entered first).

The most important rule: work out the expected result by hand before you run the test. If you look at the program's output first, you will tend to believe it.

valid marks: 0 to 100-5050100invalidinvalidboundaries: -1, 0, 1pass boundary: 49, 50, 51boundaries: 99, 100, 101typical: 30typical: 80
Choosing test values for a program that accepts marks from 0 to 100 and passes 50 or more: typical values inside each region, values at and on each side of every boundary, and invalid values outside the range.
Worked example

Example 2: A test plan

A shipping program reads an order total. Orders of $50 or more ship free; orders under $50 cost $8.99 to ship; a total below $0 should print Invalid. Write a test plan.

  1. List the conditions: invalid if total < 0; free if total ≥ 50; otherwise $8.99.
  2. Choose cases: a typical value in each branch (20, 75), the boundaries (49.99, 50, 50.01, 0, −0.01) and an invalid value (−10).
  3. Write the expected output for each before running the program.
  4. Run each test; record the actual output and mark Pass or Fail. Any Fail means a bug (or a mistake in your expected result, which you should double-check).

See the table below. If the program used > 50 by mistake, the test with exactly 50 would fail, which is exactly why boundary cases matter.

#InputType of caseExpected outputActualPass?
120typical (paid)Shipping: $8.99
275typical (free)Shipping: free
349.99boundaryShipping: $8.99
450boundaryShipping: free
550.01boundaryShipping: free
60boundary / extremeShipping: $8.99
7-0.01boundary (invalid)Invalid
8-10invalidInvalid
A test plan for Example 2. The Actual and Pass columns are filled in while testing.
FREE_SHIPPING_MIN = 50
SHIPPING_FEE = 8.99
total = float(input("Order total: $"))
if total < 0:
print("Invalid")
elif total >= FREE_SHIPPING_MIN:
print("Shipping: free")
else:
print(f"Shipping: ${SHIPPING_FEE:.2f}")

The program for Example 2, run on test case 4. All 8 cases in the plan pass with this code. Output, line by line: Order total: $50 | Shipping: free (the typed input is shown after each prompt)

Clean code: professional conventions

Code is read far more often than it is written. Expectation A4.2 asks you to use workplace conventions. Check every program against this list:

  • Header comment: file name, author, date, purpose.
  • Meaningful names in snake_case; UPPER_CASE constants instead of magic numbers.
  • 4-space indentation, never mixed with tabs.
  • Comments that explain why or summarize a section, not ones that repeat the code.
  • Blank lines between sections (constants, input, processing, output).
  • Spaces around operators and after commas: total = price * qty, not total=price*qty.
  • Clear prompts and labelled output.

Python's official style guide is called PEP 8. Tools in many IDEs can check it automatically.

hard to readcleana=float(input())b=a*0.13c=a+bif c>100:  print('x')print(c)# Price with HST; flags big ordersHST_RATE = 0.13BIG_ORDER = 100price = float(input("Price: $"))total = price + price * HST_RATEif total > BIG_ORDER:    print("Big order!")print(f"Total: ${total:.2f}")no comment, no clear names,magic numbers, 2-space indent
The same program twice. Both run, but the clean version explains itself: a comment, named constants instead of "magic numbers", meaningful names, 4-space indentation, a clear prompt and formatted output.
Just for fun

A software tester walks into a café. She orders 1 coffee. Then 0 coffees. Then 99 999 999 coffees. Then −1 coffees. Then "a lizard".

The first real customer asks where the washroom is, and the café's ordering system crashes. Nobody tested that.

Hands-on activity

Test plan and code review

You need: Python, one program you wrote earlier in this unit (the level calculator, the clothing program or the sentinel average are good choices), paper, and a family member.

  1. Write a test plan table with at least 8 cases: typical, boundary, extreme and invalid. Fill in the expected outputs before running anything.
  2. Run every case. Record the actual output and Pass/Fail. Fix any bugs you find and rerun all the tests (a fix can break something else).
  3. Clean the code using the checklist in this lesson.
  4. Code review: read your program aloud to a family member, line by line. Every time they cannot guess what a name means, rename it. Ask them to choose one extra test value to try.

What to look for: at least one boundary test for every condition; expected results written before running; every failed test either fixed or explained; after cleaning, the program has a header comment, named constants and no variables like a, x2 or temp2 whose purpose is unclear.

Your turn

1

Make a trace table and give the output.

  • x = 10
  • count = 0
  • while x > 1:
  •     x = x // 2
  •     count += 1
  • print(x, count)
2

A program decides if age >= 65: gives a senior discount. List three boundary test values and the expected result of each.

3

What test inputs make a sentinel loop run 0 times, once and several times? (Sentinel: 0.)

4

Why must expected results be worked out before running a test?

5

Rewrite in clean style: x=float(input()) / y=x*1.13 / print(y) (it adds 13% HST to a price).

6

Add a debug print to find out why this prints 0: product = 0, then for i in range(1, 5): product *= i.

7

Give one invalid test case for a program that reads a number of people for a restaurant booking, and say what should happen.

More practice: warm-up

1

What does each row of a trace table show?

2

Name the four kinds of test cases in this lesson.

3

What is a magic number?

4

How many spaces does PEP 8 recommend for each indentation level?

5

What should you do with debug prints when the bug is fixed?

More practice: on your own

1

Trace and give the output.

  • a = 0
  • b = 1
  • for i in range(4):
  •     a, b = b, a + b
  • print(a, b)
2

Write a test plan (inputs and expected outputs) for a program that prints child under 13, teen from 13 to 19 and adult from 20, and invalid for negative ages.

3

The specification says 90 to 100 is an A and 80 to 89 is a B. Tests with 85 (B) and 95 (A) pass. Which boundary test reveals the bug, and what is the fix?

  • if 90 < mark <= 100:
  •     print("A")
  • elif mark >= 80:
  •     print("B")
4

List three things you would change to make this code meet the conventions: T=0, for X in range(5):, T=T+int(input()), print(T/5).

5

Why should you rerun all tests after fixing one bug?

Challenge

1

A program finds the largest of numbers entered until the sentinel 0. Design a test plan of at least 6 cases that would catch: starting largest at 0, a sentinel counted as data, and the largest value being first or last.

2

Trace this program with input 5 and give the output. Then describe what it computes.

  • n = int(input())
  • result = 0
  • i = 1
  • while i <= n:
  •     if i % 2 == 1:
  •         result += i * i
  •     i += 1
  • print(result)
3

Explain why testing can show that a program has bugs, but can never prove it has none, and what developers do about it.

Check your understanding

Quiz

1.

For if speed > 100:, which set of tests checks the boundary best?

2.

When should expected results be written in a test plan?

3.

After x = 3, the loop while x < 20: x = x * 2 runs. What is x after the loop?

4.

Which is a magic number problem?

5.

A sentinel loop should be tested so that it runs:

6.

What is the purpose of a temporary print("DEBUG", x)?

For parents and teachers

Big idea: expectations A4.4 (tracing to understand program flow and correct errors), A4.5 (validating a program with a full range of test cases) and A4.2 (professional conventions), with continued practice of A4.1.

How to help: testing is a perfect job for a parent. Ask for the test plan and pick a few rows: "Why did you expect that answer?" Then try a value they did not think of. Reading code aloud together is a real professional practice called code review.

Common difficulties:

  • Testing only "nice" values and skipping boundaries and invalid input.
  • Writing expected results after seeing the output.
  • Not retesting everything after a fix.
  • Leaving debug prints in finished programs.