Testing Functions
Learning Objectives
- You can use
assertto check a function call against an expected result. - You can choose normal, boundary, empty, and otherwise revealing test cases.
- You can interpret a failing assertion as evidence that an expectation was not met.
- You can explain why calculation functions are easier to test than whole interactive programs.
- You can use tests while refactoring to check that behavior has stayed the same.
You have already tested programs by checking boundaries, empty sequences, no-match cases, and other revealing inputs. Functions make this habit easier to automate.
A Test Compares Actual and Expected Behavior
Run this example:
assert records an expectation directly. Read it as:
I expect
double(5)to equal10.
If the expression after assert is true, Python continues silently and the example reaches its print call.
If it is false, Python raises an AssertionError.
Change the expected 10 to 11 and run the example again. The assertion fails because the function returns 10.
When a program is run normally, a failed assertion stops it at that statement, so statements later in the program are not run.
Passing tests need no success report; a failure draws attention to the unmet expectation.
Build a Revealing Test Set
This test is useful:
assert double(5) == 10but it only checks one input.
A more informative set might be:
assert double(5) == 10assert double(0) == 0assert double(-3) == -6These cover:
- an ordinary positive value;
- zero;
- a negative value.
The categories depend on the function. Ask:
Which inputs are most likely to reveal that my implementation does not match the intended behavior?
You can group related assertions in an ordinary function. For the double function from the start of the chapter, place this after its definition:
def check_double(): assert double(8) == 16
check_double()Defining check_double does not run its assertion. Calling it runs the check. Add more assertions to the function to check more inputs; they run in order until one fails or all have passed.
Write a few checks of your own, then trace what happens when an assertion fails:
Boundary Cases from Part 1
Consider:
def ticket_price(age): if age < 7: return 0 elif age < 18: return 8 else: return 12A weak test set might be:
assert ticket_price(5) == 0assert ticket_price(12) == 8assert ticket_price(30) == 12Those tests cover each branch, but not the exact boundaries:
assert ticket_price(6) == 0assert ticket_price(7) == 8assert ticket_price(17) == 8assert ticket_price(18) == 12These assertions record and rerun checks at the points where the result changes.
Testing Functions That Process Lists
Consider:
def count_positive(values): count = 0
for value in values: if value > 0: count = count + 1
return countA useful test set could include:
assert count_positive([4, -2, 7]) == 2assert count_positive([1]) == 1assert count_positive([-4, -2]) == 0assert count_positive([]) == 0assert count_positive([0, 1, 0]) == 1These cases ask:
- Does ordinary mixed input work?
- Does a single item work?
- What happens when there are no matches?
- What happens when the list itself is empty?
- Is zero correctly excluded by
> 0?
They turn familiar edge-case reasoning into executable checks.
Testing a Function That Mutates a List
Some functions return a new result. Others deliberately change a mutable value that the caller supplied. In that case, test the changed value:
def replace_negative_values_with_zero(values): for index in range(len(values)): if values[index] < 0: values[index] = 0
temperatures = [4, -2, 7, -1]replace_negative_values_with_zero(temperatures)assert temperatures == [4, 0, 7, 0]The assertion checks temperatures after the call because the function’s stated responsibility is mutation, not returning a list.
An empty list is useful here too:
temperatures = []replace_negative_values_with_zero(temperatures)assert temperatures == []Test the intended interface: inspect a returned result or, when mutation is intended, the changed object.
Tests Should Match the Intended Behavior
Suppose you write:
def is_long_session(minutes): return minutes > 30Then you write:
assert is_long_session(30) == FalseThe test passes.
But what if the actual requirement was:
A session of at least 30 minutes is long.
Now both the implementation and the test are wrong in the same way.
A test only checks the expectation you wrote down. It does not decide whether that expectation was the right one.
As in the first exercise, group related assertions in a check function and call it after the definitions.
Investigate a small function whose supplied test passes. Choose more tests from its required behavior, then repair the implementation:
Badge scanner audit
0 / 20 points
The starter contains has_access_code(codes, required_code). Its required behavior is:
Return
Truewhenrequired_codeoccurs anywhere in the listcodes. ReturnFalsewhen it does not occur or whencodesis empty. Compare the strings exactly. Do not print anything or changecodes.
The supplied test passes, but the implementation does not meet this specification. Find an input that reveals a problem and add an assertion for the expected result. Run it before repairing the function.
Repair the search and choose additional tests from the specification. Your tests should distinguish a correct search from one that gives wrong answers for some inputs. Practise the loop and return placement from Chapter 5.
Keep the supplied assertion and put your additional assertions inside check_access_codes(). This function takes no arguments and runs your checks. Keep its call at the end of the program. The complete program should be silent when all tests pass.
Why Functions Are Easier to Test Than Whole Interactive Scripts
Repeatedly testing interactive read_sessions requires typing values, stopping the loop, and inspecting output.
Compare that with an assertion for count_positive from earlier in this chapter:
assert count_positive([4, -2, 7]) == 2The input and expectation are visible together and can be rerun after every change.
- functions that perform calculations from parameters are straightforward to call from tests;
- functions whose main purpose is keyboard input or printing usually require more elaborate testing techniques.
For now, focus automated tests on the calculation logic.
Also run the complete program through representative interactions. Helper tests do not check whether input, calculations, and output are connected correctly.
Testing Before and After Refactoring
When refactoring, use the same inputs before and after the change. For example, run the festival dashboard with an empty line, one count, and a mixture of busy and quiet periods, and keep the output for comparison.
After extracting a calculation, also test it directly, as you did with count_positive earlier in this chapter. Once those checks pass, run the complete program again to check that it uses the result correctly.
Passing the same cases provides evidence that the refactoring preserved behavior, though it is not proof. If you introduced a parameter, test more than the value the original script used.
Choose tests that expose boundary, search, empty-input, and refactoring mistakes:
The assertions in this chapter are checks that a programmer runs while developing a program. They do not replace if conditions or the input-validation techniques in the next chapter.
For example, assert ticket_price(7) == 8 checks the implementation. It does not prevent a user from entering text when the program expects an age.
Check Your Understanding
- What happens when the expression in an
assertstatement is false? - Why are boundary values often useful test inputs?
- Why can one ordinary example fail to reveal an early-return bug in a loop?
- Why are calculation functions usually easier to test than functions that call
input(...)? - Why can a program and its tests both agree with each other and still violate the actual requirement?
Chapter 8 next applies function design to safe interactive input. Chapter 9 then returns to the relationship between implementations, tests, and requirements.
AI Study Aid
Create a chapter diagram
We're looking into to what extent AI-generated diagrams could help with studying.
Use this study aid to generate an AI-generated visual summary of the material you just studied.
Each diagram style emphasizes the content in a different way, so you can choose the focus
that feels most useful.
Using the diagram generator is voluntary and does not affect your course progress. We encourage
you to try it out and see if it helps, but it's totally up to you! Your answers help us understand
how to make better study aids in the future.