Python unittest
Python comes with a complete testing framework in its standard library: unittest. You do not need to install anything, and many company codebases and older projects use it, so every tester and developer working with Python should be able to read and write unittest tests.
In this lesson you will write test classes, use the most common assert methods, prepare fresh test data with setUp(), run data-driven checks with subTest(), skip tests, read a failure report and run tests from the terminal.
Your first test class
A unittest test is a method inside a class that inherits from unittest.TestCase. Every method whose name starts with test is a test. Inside it you call assert methods such as self.assertEqual(actual, expected).
import unittest
def add_gst(price, rate=18):
if price < 0:
raise ValueError("price cannot be negative")
return round(price * (100 + rate) / 100, 2)
class TestAddGst(unittest.TestCase):
def test_default_rate_is_18_percent(self):
self.assertEqual(add_gst(100), 118.0)
def test_custom_rate(self):
self.assertEqual(add_gst(1299, rate=5), 1363.95)
def test_zero_price(self):
self.assertEqual(add_gst(0), 0)
def test_negative_price_is_rejected(self):
with self.assertRaises(ValueError):
add_gst(-50)
suite = unittest.TestLoader().loadTestsFromTestCase(TestAddGst)
unittest.TextTestRunner(verbosity=2).run(suite)test_custom_rate (__main__.TestAddGst.test_custom_rate) ... ok test_default_rate_is_18_percent (__main__.TestAddGst.test_default_rate_is_18_percent) ... ok test_negative_price_is_rejected (__main__.TestAddGst.test_negative_price_is_rejected) ... ok test_zero_price (__main__.TestAddGst.test_zero_price) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.032s OK
The time in the "Ran 4 tests" line will be different every time you run it. Notice that tests run in alphabetical order of their names, not in the order you wrote them, which is a reminder that tests must not depend on each other. assertRaises used with with passes only if the code inside raises the named exception.
Common assert methods
| Method | Passes when |
|---|---|
assertEqual(a, b) / assertNotEqual(a, b) | a == b / a != b |
assertTrue(x) / assertFalse(x) | x is truthy / falsy |
assertIs(a, b) / assertIsNone(x) | a is b / x is None |
assertIn(a, b) / assertNotIn(a, b) | a in b / a not in b |
assertAlmostEqual(a, b, places=2) | a and b are equal when rounded to 2 places |
assertGreater(a, b) / assertLess(a, b) | a > b / a < b |
assertRaises(Error) | the code raises Error |
assertRaisesRegex(Error, "text") | the code raises Error and its message matches text |
assertCountEqual(a, b) | a and b contain the same items in any order |
Prefer the specific method over assertTrue(a == b): when it fails, assertEqual shows both values, while assertTrue only says False is not true.
setUp and tearDown
Most tests need some objects to work with. Put that preparation in setUp(), which unittest calls before every test, so each test gets a fresh copy and cannot be affected by another. tearDown() runs after every test for clean-up such as deleting temporary files. This example also contains a real defect; run it and read the failure report.
import unittest
class Cart:
def __init__(self):
self.items = {}
def add(self, name, price, qty=1):
if qty <= 0:
raise ValueError("quantity must be at least 1")
old_qty = self.items.get(name, (price, 0))[1]
self.items[name] = (price, qty) # defect: forgets old_qty
def total(self):
return sum(price * qty for price, qty in self.items.values())
class TestCart(unittest.TestCase):
def setUp(self):
self.cart = Cart() # fresh cart for every test
self.cart.add("notebook", 60, qty=2)
def test_total_of_one_line(self):
self.assertEqual(self.cart.total(), 120)
def test_adding_same_item_increases_quantity(self):
self.cart.add("notebook", 60, qty=3)
self.assertEqual(self.cart.items["notebook"], (60, 5))
def test_zero_quantity_is_refused(self):
with self.assertRaisesRegex(ValueError, "at least 1"):
self.cart.add("pen", 10, qty=0)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCart)
unittest.TextTestRunner(verbosity=2).run(suite)test_adding_same_item_increases_quantity (__main__.TestCart.test_adding_same_item_increases_quantity) ... FAIL test_total_of_one_line (__main__.TestCart.test_total_of_one_line) ... ok test_zero_quantity_is_refused (__main__.TestCart.test_zero_quantity_is_refused) ... ok ====================================================================== FAIL: test_adding_same_item_increases_quantity (__main__.TestCart.test_adding_same_item_increases_quantity) ---------------------------------------------------------------------- Traceback (most recent call last): File "<exec>", line 26, in test_adding_same_item_increases_quantity AssertionError: Tuples differ: (60, 3) != (60, 5) First differing element 1: 3 5 - (60, 3) ? ^ + (60, 5) ? ^ ---------------------------------------------------------------------- Ran 3 tests in 0.022s FAILED (failures=1)
Read a failure report from the bottom of its block upwards: the AssertionError line says the actual tuple was (60, 3) but (60, 5) was expected, and the line above says which test and line failed. The fix is self.items[name] = (price, old_qty + qty). In a real file the traceback shows the file name and the source line instead of <exec>.
There are also class-level versions, setUpClass() and tearDownClass(), that run once for the whole class. Use them only for expensive, read-only set-up such as starting a test database, because shared objects can leak changes between tests.
subTest and skipping
When one test checks many values, a plain loop stops at the first failure and hides the others. with self.subTest(...) reports every failing value separately and labels it. You can also skip tests with decorators such as @unittest.skip("reason"), @unittest.skipIf(condition, "reason") and @unittest.expectedFailure.
import unittest
def grade(marks):
if not 0 <= marks <= 100:
raise ValueError("marks must be 0-100")
if marks > 90: # defect: 90 should be an A
return "A"
if marks >= 75:
return "B"
if marks >= 40:
return "C"
return "Fail"
class TestGrade(unittest.TestCase):
def test_boundaries(self):
cases = [(100, "A"), (90, "A"), (89, "B"), (75, "B"),
(74, "C"), (40, "C"), (39, "Fail"), (0, "Fail")]
for marks, expected in cases:
with self.subTest(marks=marks):
self.assertEqual(grade(marks), expected)
@unittest.skip("grace marks rule not decided yet")
def test_grace_marks(self):
self.assertEqual(grade(38), "C")
suite = unittest.TestLoader().loadTestsFromTestCase(TestGrade)
unittest.TextTestRunner(verbosity=2).run(suite)test_boundaries (__main__.TestGrade.test_boundaries) ... test_boundaries (__main__.TestGrade.test_boundaries) (marks=90) ... FAIL test_grace_marks (__main__.TestGrade.test_grace_marks) ... skipped 'grace marks rule not decided yet' ====================================================================== FAIL: test_boundaries (__main__.TestGrade.test_boundaries) (marks=90) ---------------------------------------------------------------------- Traceback (most recent call last): File "<exec>", line 20, in test_boundaries AssertionError: 'B' != 'A' - B + A ---------------------------------------------------------------------- Ran 2 tests in 0.020s FAILED (failures=1, skipped=1)
The label (marks=90) points straight at the failing boundary, and the other seven values were still checked. Always give a reason when skipping, so nobody forgets why.
Tests in files and the terminal
In a real project, code and tests live in separate files. By convention test files are named test_*.py:
# test_billing.py
import unittest
from billing import add_gst
class TestAddGst(unittest.TestCase):
def test_default_rate_is_18_percent(self):
self.assertEqual(add_gst(100), 118.0)
def test_negative_price_is_rejected(self):
with self.assertRaises(ValueError):
add_gst(-50)
if __name__ == "__main__":
unittest.main()You can run this file directly with python test_billing.py, but the usual way is test discovery: from the project folder, python -m unittest finds and runs every test*.py file. Add -v to see each test by name:
python -m unittest -vtest_default_rate_is_18_percent (test_billing.TestAddGst.test_default_rate_is_18_percent) ... ok test_negative_price_is_rejected (test_billing.TestAddGst.test_negative_price_is_rejected) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
Without -v, each passing test prints a single dot, F marks a failure, E an error (an unexpected exception inside the test) and s a skipped test. The command exits with a non-zero code when any test fails, which is how build servers know the build is broken.
Common mistakes
- Method names without the
testprefix.def check_total(self)is silently ignored. - Tests that depend on each other. Tests run in name order, and a test must never rely on another one having run first. Use
setUp(). - Comparing floats exactly. Use
assertAlmostEqualor round the result. - Catching the exception yourself. A
try/exceptinside a test that swallows errors makes the test pass when it should fail; useassertRaises.
Key points
- unittest is built into Python; tests are
test*methods in classes that inherit fromunittest.TestCase. - Use specific assert methods such as
assertEqual,assertInandassertRaisesfor clear failure messages. setUp()andtearDown()run before and after every test, giving each test a fresh state.subTest()reports each failing value of a data-driven test separately; decorators skip tests with a reason.- Run tests with
python -m unittest -v; a failed run exits with a non-zero code.
Exercise
Write a TestCase for a function split_bill(total, people) that divides a restaurant bill equally and rounds to 2 decimal places, raising ValueError if people is less than 1. Include tests for ₹1,200 among 4 people, ₹1,000 among 3 people, 1 person, and 0 people. Use assertAlmostEqual for one of the checks.
Show solution
import unittest
def split_bill(total, people):
if people < 1:
raise ValueError("at least one person must pay")
return round(total / people, 2)
class TestSplitBill(unittest.TestCase):
def test_even_split(self):
self.assertEqual(split_bill(1200, 4), 300.0)
def test_uneven_split_is_rounded(self):
self.assertAlmostEqual(split_bill(1000, 3), 333.33, places=2)
def test_one_person_pays_everything(self):
self.assertEqual(split_bill(850, 1), 850.0)
def test_zero_people_is_refused(self):
with self.assertRaises(ValueError):
split_bill(500, 0)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSplitBill)
unittest.TextTestRunner(verbosity=2).run(suite)All four tests pass. The zero-people test protects against a ZeroDivisionError reaching the user: the function raises a clear ValueError before it ever divides.
Quick Quiz
1. Which method name will unittest run as a test?
unittest collects methods whose names start with test.
2. When does setUp() run?
setUp() runs before each test so every test starts from a fresh state; setUpClass() runs once.
3. What is the benefit of self.subTest() inside a loop?
subTest labels and reports each failure while the remaining values are still checked.
4. What does E mean in unittest's short output?
F is a failed assertion, E is an error such as an unexpected exception, s is skipped.
Learn Software Testing with a mentor
Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
