Python · Course navigation

Unit testing with unittest

Write small automated tests that check normal behavior, boundaries, and error handling.

On this page
Before you start

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

Python
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())
Output
3 True

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1import io
2import unittest
3
4
5def apply_discount(price_cents, percent):
6 if not 0 <= percent <= 100:
7 raise ValueError("percent must be between 0 and 100")
8 return price_cents * (100 - percent) // 100
9
10
11class DiscountTests(unittest.TestCase):
12 def test_typical_discount(self):
13 self.assertEqual(apply_discount(8000, 25), 6000)
14
15 def test_zero_discount_keeps_price(self):
16 self.assertEqual(apply_discount(1999, 0), 1999)
17
18 def test_invalid_percent_is_rejected(self):
19 with self.assertRaises(ValueError):
20 apply_discount(1000, 120)
21
22
23suite = unittest.defaultTestLoader.loadTestsFromTestCase(DiscountTests)
24result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
25print(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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading