Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 9 of 10

Right now you test by running the program and looking at the output. That works until the program has twenty functions and you change one of them. Automated tests are code that checks your code — you run them in a second and they tell you exactly what broke.

The idea, at its simplest

def add(a, b):
    return a + b


assert add(2, 3) == 5
assert add(-1, 1) == 0
print("all good")

assert raises AssertionError if the condition is false and does nothing if it's true. That's already a test. Real test frameworks just organise thousands of these and report nicely.

unittest

unittest is in the standard library — nothing to install.

import unittest


def add(a, b):
    return a + b


class TestAdd(unittest.TestCase):
    def test_positive(self):
        self.assertEqual(add(2, 3), 5)

    def test_negative(self):
        self.assertEqual(add(-1, -1), -2)

The rules:

  1. Test class inherits from unittest.TestCase.
  2. Every test method's name starts with test_ — that's how they get discovered.
  3. Each method makes assertions with self.assert....

Save that as test_math.py and run it from your terminal:

python -m unittest

unittest walks the folder, finds files named test_*.py, finds TestCase classes inside them, and runs every test_ method.

The assertion methods

| Method | Passes when | | --- | --- | | assertEqual(a, b) | a == b | | assertNotEqual(a, b) | a != b | | assertTrue(x) / assertFalse(x) | x is truthy / falsy | | assertIsNone(x) / assertIsNotNone(x) | x is None / isn't | | assertIn(a, b) | a in b | | assertAlmostEqual(a, b) | floats are close enough | | assertRaises(Error) | the block raises that error | | assertIsInstance(x, T) | isinstance(x, T) |

Use the specific one. assertEqual(total, 100) prints 100 != 95 when it fails; assertTrue(total == 100) prints only False is not true.

assertAlmostEqual exists because floats don't compare exactly — 0.1 + 0.2 != 0.3.

Testing that something raises

import unittest


def divide(a, b):
    if b == 0:
        raise ValueError("cannot divide by zero")
    return a / b


class TestDivide(unittest.TestCase):
    def test_raises_on_zero(self):
        with self.assertRaises(ValueError):
            divide(1, 0)

The test passes when the error is raised. Error paths need tests as much as happy paths — usually more, since nobody exercises them by hand.

setUp and tearDown

setUp runs before every test method, tearDown after. Use them for shared fixtures:

import unittest


class TestCart(unittest.TestCase):
    def setUp(self):
        self.cart = []

    def test_starts_empty(self):
        self.assertEqual(len(self.cart), 0)

    def test_can_add(self):
        self.cart.append("mouse")
        self.assertEqual(len(self.cart), 1)

Each test gets a fresh self.cart. That matters: tests must not depend on each other or on the order they run in. One test leaking state into the next is the classic reason a suite "passes locally but fails in CI".

What pytest adds

pytest is third-party (pip install pytest) and it's what most projects actually use. Same job, less ceremony — plain functions and plain assert:

# test_math.py
from mymath import add, divide
import pytest


def test_add():
    assert add(2, 3) == 5


def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(1, 0)

Run it with pytest. No classes, no self.assertEqual — pytest rewrites assert so a failure still shows you both sides.

Two features worth knowing about:

import pytest


@pytest.fixture
def cart():
    return []


def test_starts_empty(cart):
    assert len(cart) == 0


@pytest.mark.parametrize("a,b,expected", [(2, 3, 5), (-1, 1, 0), (0, 0, 0)])
def test_add_many(a, b, expected):
    assert a + b == expected

A fixture is setUp as a reusable function — ask for it by name in the parameter list. parametrize runs the same test once per row of data, and reports each row separately.

pytest also runs unittest test classes unchanged, so migrating is painless.

The exercises below use unittest because it's in the standard library and runs right here in your browser. pytest needs installing, so try it in a real project — the concepts transfer exactly.

Running tests inside a script

Normally you run tests from the terminal. To run them here, load the class into a suite and run it yourself — that's all python -m unittest does under the hood.

Try it yourself

What a failure looks like

One test below is deliberately wrong. Notice the suite still runs every other test — a failure doesn't stop the run.

Fix the expected total to 200 and re-run — everything passes. That loop of red, fix, green is what testing feels like in practice.

What to test

You can't test everything, so aim at what breaks:

  • The happy path — the normal case works.
  • The edges — empty list, zero, one item, the maximum, negative numbers.
  • The errors — bad input raises what you promised it would.
  • Every bug you fix — write the test that fails first, then fix it. That bug never comes back.

And structure each test as arrange, act, assert: set up the data, run the thing, check the result. One behaviour per test, and a name that says what it checks — test_negative_price_rejected beats test_2.

Don't test the standard library, and don't chase 100% coverage. Coverage measures which lines ran, not whether they're correct.

Checkpoint

  • A test is code that checks your code; assert is the whole idea in one keyword.
  • unittest: subclass TestCase, name methods test_*, assert with self.assertEqual and friends. Run with python -m unittest.
  • assertRaises tests the error path; assertAlmostEqual tests floats.
  • setUp runs before each test so tests stay independent.
  • pytest does the same with plain functions, plain assert, fixtures, and parametrize.
  • Test the happy path, the edges, the errors, and every bug you've ever fixed.

Questions about this lesson

Log in to ask a question.