Unit testing with unittest
Write small automated tests that check normal behavior, boundaries, and error handling.
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
Explanation
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.
Explore the dry run
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 |
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.