CSC108H1/Unit 8
Unit 8 · Intro to Programming

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.

By the end you can
  • Run doctest on your own docstrings.
  • Write a unittest test case with several checks.
  • Choose test inputs by category rather than at random.
  • Say why testing a float with == is a mistake.
Why it matters & where it comes from
Why it exists

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.

In the real world

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.

Story & open questions

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

Definition

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.

Example Running your docstrings
python
def double(n: int) -> int:
    """Return twice n.

    >>> double(4)
    8
    >>> double(0)
    0
    """
    return n * 2

if __name__ == '__main__':
    import doctest
    doctest.testmod()
Silence means every example passed. A failure prints what was expected and what happened.
The if __name__ == '__main__': guard makes the tests run when the file is executed directly but not when it is imported by another module.
The expected output must match exactly

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

Definition

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.

Example A test file
python
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()
Each method tests one thing and says which in its name, so a failure report names the broken case.
Useful assertions: assertEqual, assertTrue, assertFalse, assertAlmostEqual for floats, and assertRaises when an error is the correct behaviour.
Never assertEqual on floats
python
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

Rule Test by category, not by whim

For any function, walk this list before writing tests:

Typical case. The ordinary input the function exists for.
Empty. Empty list, empty string, zero items.
One item. Boundaries often break where the loop runs exactly once, or not at all.
Boundary values. Zero, one, the first and last position, the exact threshold in a comparison.
Negative or unusual. Negative numbers, duplicates, already-sorted input, all-equal input.

Two well-chosen cases beat ten random ones. The categories are also how assignment graders choose theirs.

Example Applying the categories

For is_sorted(xs) from Unit 2:

python
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.

Common pitfalls

• 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.

Practice 1 · Why does it fail?

Why does this doctest fail?

python
def shout(s: str) -> str:
    """Return s in upper case.

    >>> shout('hi')
    HI
    """
    return s.upper()
Answer
The shell displays a returned string with quotes, so the expected line must be 'HI'. doctest compares text exactly.
Practice 2 · Categories

List five test cases for count_vowels(s).

Answer
Typical ('hello'), empty (''), no vowels ('xyz'), all vowels ('aeiou'), mixed case ('HeLLo'). The last checks a decision the docstring should already state.
Practice 3 · Float assertion

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.
Practice 4 · Write tests

Write a unittest case for is_palindrome from Unit 4.

Solution
python
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(''))
Practice 5 · Expected failure

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.
Practice 6 · Test independence

Why should tests not depend on each other's order?

Answer
The framework may run them in any order, and one failing test would cascade into unrelated failures. Each should set up whatever it needs.
Practice 7 · Passing tests, broken code

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.
Practice 8 · Smallest failing case

Why prefer the smallest input that reproduces a bug?

Answer
Because the fewer moving parts, the more directly the failure points at the cause. Shrinking a failing input is usually faster than reading the code again.

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 assertAlmostEqual exists.
Textbook/source alignment

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.

Core idea map

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.

Study routine. Before fixing any bug, write the failing test first. You then know when it is actually fixed, and it can never come back unnoticed.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · What doctest reads

Where does doctest find its tests?

Guide
In docstrings, on lines beginning >>>, comparing the next line to the actual output.
Extension 2 · Float comparison

Which assertion for floats?

Guide
assertAlmostEqual, which compares within a tolerance rather than exactly.
Extension 3 · Case categories

Name the five categories.

Guide
Typical, empty, single item, boundary values, and unusual input such as negatives or duplicates.
Extension 4 · What tests prove

Can passing tests prove code correct?

Guide
No. They show bugs are present when they fail; passing only means the cases you chose did not expose one.