Learning Goals
3 min- Write a
unittest.TestCasesubclass withtest_*methods. - Use
self.assertEqualand friends instead of bareassert. - Run tests with
python -m unittest. - Read the dots, the F/E markers, and the summary.
Warm-Up · No Install Required
5 minunittest is part of the standard library — it's already on your machine. It's based on the classic "xUnit" style: tests are methods inside a class that inherits from TestCase.
A TestCase subclass is your test runner's home. Each method named test_* is one test, auto-discovered. Inside, you use self.assertEqual(...) and friends — which give better failure messages than bare assert (they print expected vs actual for you).
New Concept · TestCase
14 minThe structure
# test_mathtools.py import unittest from mathtools import is_prime, factorial class TestMathTools(unittest.TestCase): def test_is_prime_true(self): self.assertTrue(is_prime(17)) def test_is_prime_false(self): self.assertFalse(is_prime(4)) def test_factorial(self): self.assertEqual(factorial(5), 120) if __name__ == "__main__": unittest.main()
Run it — two ways
$ python test_mathtools.py # if you added unittest.main() $ python -m unittest test_mathtools # the standard way $ python -m unittest # auto-discover all test_*.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK
Reading the output
. a passing test (one dot per test) F a failed assertion E an unexpected error (exception) ...F. means: 3 pass, 1 fail, 1 pass
Why self.assertEqual beats assert
# bare assert — unhelpful on failure assert factorial(5) == 120 # unittest assertion — prints expected vs actual automatically self.assertEqual(factorial(5), 120)
# when it fails, unittest tells you: AssertionError: 119 != 120
You get the actual value (119) and the expected (120) for free — no need to write your own message.
The verbose flag
$ python -m unittest -v test_factorial (test_mathtools.TestMathTools) ... ok test_is_prime_false (...) ... ok test_is_prime_true (...) ... ok
Worked Example · Testing a BankAccount Class
12 min# bank.py class BankAccount: def __init__(self, balance=0): self.balance = balance def deposit(self, amount): if amount <= 0: raise ValueError("deposit must be positive") self.balance += amount def withdraw(self, amount): if amount > self.balance: raise ValueError("insufficient funds") self.balance -= amount
# test_bank.py import unittest from bank import BankAccount class TestBankAccount(unittest.TestCase): def test_starts_at_zero(self): self.assertEqual(BankAccount().balance, 0) def test_deposit_increases_balance(self): acc = BankAccount() acc.deposit(100) self.assertEqual(acc.balance, 100) def test_withdraw_decreases_balance(self): acc = BankAccount(100) acc.withdraw(40) self.assertEqual(acc.balance, 60) def test_overdraw_raises(self): acc = BankAccount(50) with self.assertRaises(ValueError): acc.withdraw(100) def test_negative_deposit_raises(self): with self.assertRaises(ValueError): BankAccount().deposit(-5) if __name__ == "__main__": unittest.main()
Run
$ python -m unittest test_bank -v test_deposit_increases_balance ... ok test_negative_deposit_raises ... ok test_overdraw_raises ... ok test_starts_at_zero ... ok test_withdraw_decreases_balance ... ok Ran 5 tests in 0.001s OK
Read the diff
Notice with self.assertRaises(ValueError): — testing an exception is now one clean line instead of the clunky try/except from Lesson 5. Each method is one focused test with a descriptive name, so a failure tells you exactly which behaviour broke. That readability is the whole point of a framework.
Try It Yourself
13 minTake the hand-rolled test file from Lesson 6 and rewrite it as a TestCase. Run with python -m unittest -v.
Write a TestCase for a small class of yours (a Counter, a Stack, a Timer). Include an assertRaises for at least one error path.
Deliberately break the code. Run the tests and read the failure block carefully — it shows the file, line, expected, and actual. Practise turning that into a one-line diagnosis.
Mini-Challenge · Test a Temperature Converter
8 minWrite a class with c_to_f, f_to_c, and a converter that rejects temperatures below absolute zero (-273.15°C). Write a TestCase covering known conversions, a round-trip, a boundary (exactly -273.15), and the error case.
Show the test skeleton
import unittest from temp import c_to_f, f_to_c, validate_c class TestTemp(unittest.TestCase): def test_known(self): self.assertEqual(c_to_f(100), 212) self.assertEqual(c_to_f(0), 32) def test_round_trip(self): self.assertAlmostEqual(f_to_c(c_to_f(37)), 37) # float-safe! def test_absolute_zero_ok(self): self.assertTrue(validate_c(-273.15)) def test_below_absolute_zero_raises(self): with self.assertRaises(ValueError): validate_c(-300) if __name__ == "__main__": unittest.main()
Non-negotiables: use assertAlmostEqual for the float round-trip (not assertEqual), test the exact boundary, and an assertRaises for invalid input.
Recap
3 minunittest is built into Python. Subclass TestCase; each test_* method is one test, auto-discovered. Use self.assertEqual / assertTrue / assertRaises for clear failure messages. Run with python -m unittest [-v]. The dots/F/E show progress at a glance. Next: the full assertion toolkit.
Vocabulary Card
- TestCase
- Base class your test classes inherit from in unittest.
- test method
- A method named
test_*inside a TestCase — one test. - self.assertEqual
- An assertion that reports expected vs actual on failure.
- assertRaises
- Context manager asserting that a block raises a given exception.
Homework
4 minPick a module with at least one class and 3+ methods. Write a full unittest.TestCase: a test per behaviour, an assertRaises for every error path, and an assertAlmostEqual if any result is a float. Run with -v and make it all green.
Model your suite on test_bank.py. One descriptive test method per behaviour; assertRaises for each error path.