Unit testing with unittest

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

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

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
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

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