unittest
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Raw assert checks are useful, but a growing project needs more structure. unittest is Python's standard-library testing module, and it gives your checks names, groups them into suites, and reports what passed or failed without any third-party package.
From asserts to test cases
In the last lesson, you wrote plain assertions like assert badge_for_score(90) == "gold". unittest keeps the same idea but wraps it in a class so Python can discover and run many checks consistently.
A test case is one named check of one behavior; in unittest, test case methods live inside a class that inherits from unittest.TestCase. A TestCase class is a class that holds related test methods, like one labeled folder for checks about score badges.
import unittest
def badge_for_score(score):
if score >= 90:
return "gold"
return "keep practicing"
class TestBadgeRules(unittest.TestCase):
def test_gold_badge_starts_at_90(self):
self.assertEqual(badge_for_score(90), "gold")
def test_lower_scores_keep_practicing(self):
self.assertEqual(badge_for_score(72), "keep practicing")
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestBadgeRules)
result = unittest.TestResult()
suite.run(result)
print("tests run:", result.testsRun)
print("failures:", len(result.failures))
print("errors:", len(result.errors))tests run: 2failures: 0errors: 0
The TestResult object stores the report. A failure is a test where an assertion method says "that claim is false." An error is an unexpected exception that stops the test before it can finish normally.
The method names start with test_ on purpose. A test loader is the part of unittest that finds test methods, and the default loader looks for that prefix. If you name a method gold_badge_starts_at_90, it is just an ordinary method; the loader skips it.
A test suite is a collection of tests that can be run together. Think of it as the day's checklist clipped onto one board: each test is one checkbox, and the suite is the board.
Assertion methods
An assertion method is a TestCase helper method that checks one claim and gives a useful failure message when the claim is false. You call it through self, because each test method is an instance method on the current TestCase object.
The most common one is assertEqual(actual, expected). You can read it as "these two values should be equal."
unittest also gives you methods for common shapes:
assertTrue(value)checks that a value is truthy.assertFalse(value)checks that Python treats a value as false.assertIn(item, collection)checks membership.assertRaises(ErrorType)checks that a block raises a specific exception.
import unittest
def badge_for_score(score):
if score < 0:
raise ValueError("score cannot be negative")
if score >= 90:
return "gold"
return "keep practicing"
def cleaned_player_name(name):
return name.strip().upper()
def is_passing(score):
return score >= 50
class TestScoreTools(unittest.TestCase):
def test_badge_message(self):
self.assertEqual(badge_for_score(91), "gold")
def test_player_name_is_cleaned(self):
self.assertEqual(cleaned_player_name(" mina "), "MINA")
def test_passing_scores(self):
self.assertTrue(is_passing(72))
self.assertFalse(is_passing(40))
def test_roster_contains_player(self):
roster = ["Mina", "Ravi", "Ada"]
self.assertIn("Ada", roster)
def test_negative_score_is_rejected(self):
with self.assertRaises(ValueError):
badge_for_score(-1)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestScoreTools)
result = unittest.TestResult()
suite.run(result)
print("tests run:", result.testsRun)
print("failures:", len(result.failures))
print("errors:", len(result.errors))tests run: 5failures: 0errors: 0
Use the method that says what you mean. assertTrue(badge_for_score(91) == "gold") works, but assertEqual(badge_for_score(91), "gold") tells the runner which two values were different if the test fails.
Running a test suite
In the browser, the examples build a suite by hand so the page can keep running. In a real project file, you usually let unittest discover tests from the terminal.
A test runner is the tool that runs a suite and reports the result. Python's built-in runner starts with python -m unittest, which means "run the unittest module as a command."
$ python -m unittestBy default, the runner searches for files named like test*.py, then looks inside them for TestCase classes and methods whose names start with test_.
For one file, name it directly:
$ python -m unittest test_badges.pyFor one class or one method, use a dotted name, a path of names joined by dots from the file to the class to the method:
$ python -m unittest test_badges.TestBadgeRules
$ python -m unittest test_badges.TestBadgeRules.test_gold_badge_starts_at_90That is the professional loop: change code, run the suite, read the report. You do not need to remember every old edge case because the suite remembers them for you.
setUp and tearDown
Sometimes several tests need the same starting data. Copying that data into every method gets noisy, and shared mutable data can leak from one test into the next.
setUp() is a special TestCase method that runs before each test method, and tearDown() is a special TestCase method that runs after each test method. They are like checking out a fresh notebook for one test and returning it when the test ends.
import unittest
def add_item(cart, item_name, price):
cart.append({"item": item_name, "price": price})
def cart_total(cart):
return sum(item["price"] for item in cart)
class TestCartTotals(unittest.TestCase):
def setUp(self):
self.cart = []
add_item(self.cart, "apples", 3)
add_item(self.cart, "bread", 4)
def tearDown(self):
self.cart = []
def test_total_adds_prices(self):
self.assertEqual(cart_total(self.cart), 7)
def test_adding_one_more_item_changes_total(self):
add_item(self.cart, "tea", 5)
self.assertEqual(cart_total(self.cart), 12)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestCartTotals)
result = unittest.TestResult()
suite.run(result)
print("tests run:", result.testsRun)
print("failures:", len(result.failures))
print("errors:", len(result.errors))tests run: 2failures: 0errors: 0
Notice the timing: setUp() runs before test_total_adds_prices, then tearDown() runs. Then setUp() runs again before test_adding_one_more_item_changes_total, then tearDown() runs again. Each test gets a fresh self.cart.
Use setUp() for repeated setup that every test in the class needs. Do not hide important differences there. If only one test needs a special item, put that item inside that one test method so the story stays visible.
Try it - add one focused test
Run this suite once. Then add a new test method that checks a score of exactly 89 returns "keep practicing". Keep the method name starting with test_, or the loader will not find it.
Checkpoint
Answer all three to mark this lesson complete
You now have the standard-library testing shape: TestCase classes, assertion methods, setup, cleanup, and a runner. Next, you meet pytest, the tool many teams choose when they want the same testing ideas with less ceremony.