Unit Testing & Design
The docstring examples you have been writing since Unit 3 are executable. This unit runs them, then goes further: choosing test cases deliberately, so that passing tests actually means something rather than confirming the one case you happened to think of.
- Run
docteston your own docstrings. - Write a
unittesttest case with several checks. - Choose test inputs by category rather than at random.
- Say why testing a float with
==is a mistake.
Code that works on the example you tried is not code that works. Tests turn "I think it is right" into a claim that can be rechecked automatically every time the code changes.
The Ariane 5 rocket was lost in 1996 to a conversion that had been safe on the previous rocket's flight profile and was never retested on the new one. The code was correct; the test cases were from the wrong world.
Dijkstra's line is that testing shows the presence of bugs, never their absence. Formal verification proves absence but is far more expensive, so almost all real software relies on well-chosen tests instead.
doctest
doctest finds lines starting with >>>
in docstrings, runs them, and compares the actual output to the text on the
following line. Any mismatch is reported.
def double(n: int) -> int:
"""Return twice n.
>>> double(4)
8
>>> double(0)
0
"""
return n * 2
if __name__ == '__main__':
import doctest
doctest.testmod()if __name__ == '__main__': guard makes the
tests run when the file is executed directly but not when it is imported by
another module.Quotes count. double('a') returning 'aa' must be
written as 'aa' with the quotes, because that is what the shell
displays. Dictionaries are worse: order was once unreliable, so the safe
pattern is to compare explicitly —
>>> invert({'a': 1}) == {1: 'a'} followed by
True.
unittest
For anything beyond a couple of examples, unittest keeps
tests in their own file. Each check is a method whose name starts with
test, and the framework finds and runs them all.
import unittest
from average import average
class TestAverage(unittest.TestCase):
"""Tests for the average function."""
def test_typical(self) -> None:
"""Test a normal list of several values."""
self.assertEqual(average([1, 2, 3]), 2.0)
def test_empty(self) -> None:
"""Test the documented empty-list behaviour."""
self.assertEqual(average([]), 0.0)
def test_single(self) -> None:
"""Test a one-item list."""
self.assertEqual(average([7]), 7.0)
def test_negatives(self) -> None:
"""Test that negative values are handled."""
self.assertEqual(average([-2, 2]), 0.0)
if __name__ == '__main__':
unittest.main()assertEqual,
assertTrue, assertFalse,
assertAlmostEqual for floats, and
assertRaises when an error is the correct
behaviour.self.assertEqual(0.1 + 0.2, 0.3) # FAILS self.assertAlmostEqual(0.1 + 0.2, 0.3) # passes
Unit 10 explains why. For now: floats are approximations, so compare them with a tolerance.
Choosing test cases
For any function, walk this list before writing tests:
Two well-chosen cases beat ten random ones. The categories are also how assignment graders choose theirs.
For is_sorted(xs) from Unit 2:
is_sorted([1, 2, 3]) # typical, True is_sorted([3, 1, 2]) # typical, False is_sorted([]) # empty - True by convention, must be documented is_sorted([5]) # one item - True, loop body never runs is_sorted([2, 2]) # duplicates - True if non-decreasing is intended is_sorted([3, 2]) # boundary - the smallest failing case
The empty and duplicate cases are decisions, not facts. Testing forces you to make them and write them down.
• Testing only the case you already know works.
• Writing tests after the code and shaping them around what it happens to
do. Write them from the docstring instead.
• Comparing floats with ==.
• Docstring examples that do not match real output, quotes included —
doctest is exact.
• One giant test method. When it fails you learn only that something
broke.
Practice
Predict each answer before running it. Original exercises written for this guide in the style of the course's weekly sets.
Why does this doctest fail?
def shout(s: str) -> str:
"""Return s in upper case.
>>> shout('hi')
HI
"""
return s.upper()Answer
'HI'. doctest compares text
exactly.List five test cases for count_vowels(s).
Answer
'hello'), empty (''), no vowels
('xyz'), all vowels ('aeiou'), mixed case
('HeLLo'). The last checks a decision the docstring should
already state.Which assertion suits average([1, 2])?
Answer
assertAlmostEqual(average([1, 2]), 1.5).
Here 1.5 is exactly representable so assertEqual
would pass, but using the tolerant form by habit avoids the cases where it
would not.Write a unittest case for is_palindrome from
Unit 4.
Solution
class TestPalindrome(unittest.TestCase):
"""Tests for is_palindrome."""
def test_simple(self) -> None:
"""Test a lowercase palindrome."""
self.assertTrue(is_palindrome('racecar'))
def test_spaces_and_case(self) -> None:
"""Test that case and spaces are ignored."""
self.assertTrue(is_palindrome('Race car'))
def test_not_palindrome(self) -> None:
"""Test a non-palindrome."""
self.assertFalse(is_palindrome('hello'))
def test_empty(self) -> None:
"""Test that the empty string counts as a palindrome."""
self.assertTrue(is_palindrome(''))How do you test that a function raises on bad input?
Answer
with self.assertRaises(ValueError): parse('abc'). The test
passes only if that error is raised.Why should tests not depend on each other's order?
Answer
Give a function passing double(2) == 4 that is
wrong.
Answer
def double(n): return n + 2. It agrees at
n = 2 and nowhere else — which is exactly why a single test
case proves nothing.Why prefer the smallest input that reproduces a bug?
Answer
Expanded topic checklist
Course coverage. This unit is part of the complete CSC108H1 topic map. It should cover the following items before you move on:
- doctest. Write runnable docstring examples and execute them.
- unittest. Test classes, test methods, the common assertions.
- Case selection. Typical, empty, single, boundary, unusual.
- Floats. Why
assertAlmostEqualexists.
The calendar names "unit testing" explicitly. Assignments in this course are commonly marked partly on the tests you supply; check the handout for which framework is required.
The docstring is the specification; tests check the code against it. That is why Unit 3 insisted on writing examples before the body — those examples were the first tests.
Deep practice extensions
Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.
Where does doctest find its tests?
Guide
>>>, comparing the next line to the actual
output.Which assertion for floats?
Guide
assertAlmostEqual,
which compares within a tolerance rather than exactly.Name the five categories.
Guide
Can passing tests prove code correct?