Legends ofPythos
Claim your name
Pythonic Python

Testing properly: unittest and doctest

Lesson 11 of 12

Watch the lesson1:11 · with Sigrid
You have already learned to check your work with assert statements. That is a good start, but it has two problems.

First, if an assertion fails, the program crashes immediately and you cannot see what would happen next.
Second, there is no summary: did 5 tests pass? Did one fail?

The standard library provides unittest, which solves both issues. It runs a collection of test methods, collects every failure, and gives you a clean report at the end.

Writing unittest tests

import unittest

def add(a: int, b: int) -> int:
    return a + b

class AddTests(unittest.TestCase):
    def test_basic(self):
        self.assertEqual(add(2, 3), 5)

    def test_negative(self):
        self.assertTrue(add(-1, -1) == -2)
A minimal unittest class
Three rules matter here:
  • The class must inherit from unittest.TestCase.
  • Every test method's name must start with the prefix test (e.g. test_basic).
  • You call assertion methods on self: assertEqual, assertTrue, assertFalse, assertIn, and others.
import unittest

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError('division by zero')
    return a / b

class DivideTests(unittest.TestCase):
    def test_zero_raises(self):
        with self.assertRaises(ValueError):
            divide(1, 0)
Checking that an exception is raised
assertRaises works as a context manager. The code inside the with block must raise that type of exception, or a subclass of it. If nothing is raised, the test fails. If a different exception is raised, it escapes the with block and unittest records the test as an error.

Running tests in this course

import unittest
import io

def add(a: int, b: int) -> int:
    return a + b

class AddTests(unittest.TestCase):
    def test_basic(self):
        self.assertEqual(add(2, 3), 5)

suite = unittest.defaultTestLoader.loadTestsFromTestCase(AddTests)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print(result.wasSuccessful())   # True or False
print(result.testsRun)          # how many tests ran
Capturing the test runner output
result.failures is a list of (test_case, traceback_string) tuples. len(result.failures) tells you how many tests failed an assertion, and each traceback string tells you why. A test that crashes with some other exception is listed in result.errors instead, so result.wasSuccessful() is the overall answer.

setUp: shared fixtures

import unittest

class CartTests(unittest.TestCase):
    def setUp(self) -> None:
        self.items = ['apple', 'banana']

    def test_has_apple(self):
        self.assertIn('apple', self.items)

    def test_length(self):
        self.assertEqual(len(self.items), 2)
setUp runs before every test method
setUp is called automatically right before each test_* method. Use it to build the state that several tests share, so you do not repeat boilerplate.

doctest: examples as documentation

def square(n: int) -> int:
    """
    >>> square(3)
    9
    >>> square(-2)
    4
    """
    return n * n
A docstring with doctest examples
Each >>> line is an expression; the lines straight after it, at the same indent as the >>>, are its expected output. To check them, call:
import doctest
doctest.run_docstring_examples(square, globals())

Which style, when?

Your turn

0 of 3 solved

Exercise 1

+40 XP
slugify is written for you. Write a unittest.TestCase subclass SlugTests with at least three test methods: one checks slugify('Hello World') is 'hello-world' with assertEqual, one checks a title with extra spaces comes out the same with assertEqual, and one uses assertIn to check a hyphen appears in slugify('A B'). Then run them the way this course does and store the result in result.
import io
import unittest




def slugify(title):
    return '-'.join(title.lower().split())




class SlugTests(unittest.TestCase):
    pass




# suite = unittest.defaultTestLoader.loadTestsFromTestCase(...)
# result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)

Run your code to check it against the tests.

Exercise 2

+40 XP
Basket is written for you. Write BasketTests(unittest.TestCase) with a setUp method that makes self.basket, a Basket holding 'apple' at 2 and 'pear' at 3, and at least three tests that use it: the count is 2, the total is 5, and adding 'plum' at 4 makes the total 9. Then run the tests and store the result in result.
import io
import unittest




class Basket:
    def __init__(self):
        self.items = {}


    def add(self, name, price):
        self.items[name] = price


    def count(self):
        return len(self.items)


    def total(self):
        return sum(self.items.values())




class BasketTests(unittest.TestCase):
    pass




result = None

Run your code to check it against the tests.

Exercise 3

+40 XP
Write initials(name), which returns the first letter of each word in capitals, each followed by a full stop: initials('Ada Lovelace') is 'A.L.'. Give it a docstring with at least two >>> examples and their results. Then import doctest and check the examples with doctest.run_docstring_examples(initials, globals()), which prints nothing when they all pass.
def initials(name):
    """Add two or more >>> examples here."""
    pass

Run your code to check it against the tests.