Unit testing with unittest
Write small automated tests that check normal behavior, boundaries, and error handling.
On this page
Functions, classes, and exceptions.
- Write small automated tests that check normal behavior, boundaries, and error handling.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A unit test checks one small piece of behavior automatically and reports pass or fail. A suite of fast, repeatable tests tells you quickly when a change breaks something. Tests also serve as executable documentation: they show how a function is expected to behave, including in unusual cases.
Python's built-in unittest module groups tests in classes that inherit from TestCase. Each method whose name starts with test is one test. A good test has three parts, often called arrange, act, and assert: set up the input, call the code, and check the result. Choose cases deliberately: a typical value, boundary values such as 0 and 100, and invalid input that should raise an error, checked with assertRaises.
The example prices are integer cents so that arithmetic is exact: floating-point numbers are a poor fit for money. The program loads the tests, runs them with a runner that writes its report to an in-memory stream, and prints how many ran and whether they all passed. In a real project you would run them with python -m unittest or another test runner such as pytest, on every change.
Code example
import io
import unittest
def apply_discount(price_cents, percent):
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return price_cents * (100 - percent) // 100
class DiscountTests(unittest.TestCase):
def test_typical_discount(self):
self.assertEqual(apply_discount(8000, 25), 6000)
def test_zero_discount_keeps_price(self):
self.assertEqual(apply_discount(1999, 0), 1999)
def test_invalid_percent_is_rejected(self):
with self.assertRaises(ValueError):
apply_discount(1000, 120)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(DiscountTests)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print(result.testsRun, result.wasSuccessful())3 True
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
import ioimport unittest def apply_discount(price_cents, percent): if not 0 <= percent <= 100: raise ValueError("percent must be between 0 and 100") return price_cents * (100 - percent) // 100 class DiscountTests(unittest.TestCase): def test_typical_discount(self): self.assertEqual(apply_discount(8000, 25), 6000) def test_zero_discount_keeps_price(self): self.assertEqual(apply_discount(1999, 0), 1999) def test_invalid_percent_is_rejected(self): with self.assertRaises(ValueError): apply_discount(1000, 120) suite = unittest.defaultTestLoader.loadTestsFromTestCase(DiscountTests)result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)print(result.testsRun, result.wasSuccessful())Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| The function states its precondition and rejects invalid input instead of returning a wrong price. | — | — | |
| Arrange 8000 cents and 25 percent, act by calling the function, and assert the expected 6000. | — | — | |
| A test can require that an error is raised: 120 percent must be rejected. | — | — | |
| Three tests ran and all of them passed. | 3 tests, 0 failures | 3 True |
Behind the scenes
How Python processes this code
Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.
Cost and common mistakes
A test run takes time proportional to the number of tests and the cost of the code they call. Unit tests should run in milliseconds.
- Tests that depend on the clock, the network, random values, or each other are flaky. Make each test independent and deterministic.
- Checking only the happy path misses most bugs. Test boundaries and invalid input too.
- A passing test suite shows that the tested cases work, not that the program is correct.
Try it yourself
Add a test for apply_discount(1000, 100). What should it return?
Reveal answer and reasoning
0. A 100 percent discount leaves nothing to pay, and 100 is a valid boundary value.