runtime
122 / 250
lesson 122 Testing & Code Quality · intermediate

unittest: test cases and a test run

unittest is the testing framework in Python's standard library. Tests are methods of a TestCase class, its assert methods explain every failure, and a run tells failures from errors.

You'll be able to
  • Write test methods in a unittest.TestCase and check results with assertEqual, assertAlmostEqual and assertIn
  • Test that code raises the right exception with with self.assertRaises(...)
  • Run a TestCase and tell a failure (a wrong answer) from an error (a crash)
The idea

unittest comes with Python, so it works everywhere Python does. Its tests live in a TestCase: a class (you'll write your own classes in the Classes & Objects module) whose methods are the tests. Each test method's name must start with test, and it takes one parameter, self, which is the running test. Through self you call the checking methods:

  • self.assertEqual(actual, expected): on failure it shows both values, so you don't need to write a message;
  • self.assertAlmostEqual(a, b): equal to 7 decimal places, for floats;
  • self.assertIn(item, collection), self.assertTrue(x), self.assertIsNone(x);
  • with self.assertRaises(ValueError): followed by an indented call: the test passes only if that call raises ValueError.

On a computer you run a test file with python -m unittest -v in a terminal. On this page, the small run function at the bottom of each example does the same with unittest's own loader and result object, and prints a summary.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Four tests of a price parser
import unittest

def parse_price(text):
    price = float(text.strip().replace("£", ""))
    if price < 0:
        raise ValueError(f"negative price: {text!r}")
    return price

class TestParsePrice(unittest.TestCase):
    def test_pound_sign(self):
        self.assertEqual(parse_price("£4.99"), 4.99)

    def test_spaces_around(self):
        self.assertEqual(parse_price("  12 "), 12.0)

    def test_negative_is_refused(self):
        with self.assertRaises(ValueError):
            parse_price("-3.50")

    def test_not_a_number(self):
        with self.assertRaises(ValueError):
            parse_price("free")

def run(case):
    result = unittest.TestResult()
    unittest.defaultTestLoader.loadTestsFromTestCase(case).run(result)
    print("ran", result.testsRun, "| failures", len(result.failures), "| errors", len(result.errors))
    for test, details in result.failures + result.errors:
        print("  " + test.id().split(".", 1)[1], "->", details.strip().splitlines()[-1])

run(TestParsePrice)

A test can go wrong in two ways, and unittest reports them separately. A failure means an assert method found a wrong answer. An error means an exception escaped from the test before it could compare: a bug in the code under test, or in the test itself.

example-2.py · A failure and an error
import unittest

def order_total(lines):
    total = 0
    for line in lines:
        total += line["unit_price"]
    return total

class TestOrderTotal(unittest.TestCase):
    def test_quantities_multiply(self):
        lines = [{"unit_price": 2.5, "quantity": 4}, {"unit_price": 10.0, "quantity": 1}]
        self.assertAlmostEqual(order_total(lines), 20.0)

    def test_empty_order(self):
        self.assertEqual(order_total([]), 0)

    def test_price_sent_as_text(self):
        lines = [{"unit_price": "2.50", "quantity": 1}]
        self.assertEqual(order_total(lines), 2.5)

def run(case):
    result = unittest.TestResult()
    unittest.defaultTestLoader.loadTestsFromTestCase(case).run(result)
    print("ran", result.testsRun, "| failures", len(result.failures), "| errors", len(result.errors))
    for test, details in result.failures + result.errors:
        print("  " + test.id().split(".", 1)[1], "->", details.strip().splitlines()[-1])

run(TestOrderTotal)

When to use it: whenever you want tests that run anywhere without installing anything, or when a project already uses unittest. (pytest, which many projects install, runs unittest TestCases too, and also plain test_ functions with assert.)

Mistakes to avoid:

  • A method whose name doesn't start with test is silently skipped: it never runs, so it never fails.
  • self.assertTrue(total == 20) reports only False is not true; assertEqual(total, 20) reports both values.
  • assertRaises without with: self.assertRaises(ValueError, parse_price("x")) calls parse_price *before* assertRaises can catch anything, so the test errors. Use the with form.
  • assertEqual on floats from arithmetic: use assertAlmostEqual.
Review
  • A unittest.TestCase holds test methods named test…, each taking self.
  • assertEqual shows both values on failure; assertAlmostEqual for floats; with self.assertRaises(E): for expected errors.
  • A failure is a wrong answer; an error is a crash. Methods not named test… never run.
  • Next: one test that checks a whole table of inputs, and reports every case that fails.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

unittest: test cases and a test run — Testing & Code Quality · Runtime