Testing with unittest
A test is a small program that checks another program. unittest ships with Python, finds your tests automatically and tells you exactly what broke.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Why test
def net_price(amount, tax_rate=0.18):
return amount * (1 + tax_rate)
print(net_price(100)) # 118.0 - looks rightOne check, run once, by hand, and never repeated. A test is that same check written down so it runs every time the code changes. The value is not proving the code works today; it is finding out the day it stops working.
The first test
# File: pricing.py
def net_price(amount, tax_rate=0.18):
if amount < 0:
raise ValueError("amount cannot be negative")
return round(amount * (1 + tax_rate), 2)# File: test_pricing.py
import unittest
from pricing import net_price
class TestNetPrice(unittest.TestCase):
def test_default_rate(self):
self.assertEqual(net_price(100), 118.0)
def test_custom_rate(self):
self.assertEqual(net_price(100, 0.05), 105.0)
def test_zero(self):
self.assertEqual(net_price(0), 0.0)
def test_negative_raises(self):
with self.assertRaises(ValueError):
net_price(-1)
if __name__ == "__main__":
unittest.main()$ python -m unittest test_pricing -v
test_custom_rate (test_pricing.TestNetPrice) ... ok
test_default_rate (test_pricing.TestNetPrice) ... ok
test_negative_raises (test_pricing.TestNetPrice) ... ok
test_zero (test_pricing.TestNetPrice) ... ok
Ran 4 tests in 0.001s
OKThe rules unittest relies on
- The file name starts with
test_. - The class inherits from
unittest.TestCase. - Each test method name starts with
test_. - Each test uses an
assert...method to state what should be true.
Running tests
python -m unittest discover and run everything
python -m unittest -v verbose
python -m unittest discover -s tests look in a specific folder
python -m unittest test_pricing one module
python -m unittest test_pricing.TestNetPrice one class
python -m unittest test_pricing.TestNetPrice.test_zero one test
python -m unittest -f stop at the first failureThe assertions
| Method | Checks |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) / assertFalse(x) | Truthiness |
assertIs(a, b) / assertIsNot | Identity |
assertIsNone(x) / assertIsNotNone | x is None |
assertIn(a, b) / assertNotIn | Membership |
assertIsInstance(a, T) | Type |
assertAlmostEqual(a, b) | Floats, to 7 places by default |
assertGreater(a, b), assertLess | Ordering |
assertRaises(E) | An exception is raised |
assertCountEqual(a, b) | Same items, any order |
import unittest
class TestAssertions(unittest.TestCase):
def test_floats(self):
self.assertAlmostEqual(0.1 + 0.2, 0.3) # not assertEqual
self.assertAlmostEqual(1.23456, 1.23459, places=4)
def test_collections(self):
self.assertCountEqual([1, 2, 3], [3, 1, 2]) # order does not matter
self.assertListEqual([1, 2], [1, 2])
self.assertDictEqual({"a": 1}, {"a": 1})
def test_exception_details(self):
with self.assertRaises(ValueError) as context:
int("abc")
self.assertIn("invalid literal", str(context.exception))
with self.assertRaisesRegex(ValueError, r"invalid literal"):
int("abc")
def test_message(self):
total = 5
self.assertEqual(total, 5, f"total was {total}, expected 5")Use the specific assertion.assertEqual(a, b)prints both values when it fails;assertTrue(a == b)prints only "False is not true", which tells you nothing.
Setup and teardown
import unittest
import tempfile
from pathlib import Path
class TestFileProcessing(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Runs once, before every test in the class."""
cls.folder = tempfile.TemporaryDirectory()
cls.root = Path(cls.folder.name)
@classmethod
def tearDownClass(cls):
cls.folder.cleanup()
def setUp(self):
"""Runs before EACH test."""
self.path = self.root / "sample.txt"
self.path.write_text("line one\nline two\n", encoding="utf-8")
def tearDown(self):
"""Runs after EACH test, even if it failed."""
self.path.unlink(missing_ok=True)
def test_line_count(self):
self.assertEqual(len(self.path.read_text(encoding="utf-8").splitlines()), 2)
def test_content(self):
self.assertIn("line one", self.path.read_text(encoding="utf-8"))Each test gets a fresh setUp, so tests never depend on each other or on the order they run in. That independence is what makes a failure meaningful.
Testing a class
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if not self._items:
raise IndexError("pop from an empty stack")
return self._items.pop()
def peek(self):
if not self._items:
raise IndexError("peek at an empty stack")
return self._items[-1]
def __len__(self):
return len(self._items)import unittest
class TestStack(unittest.TestCase):
def setUp(self):
self.stack = Stack()
def test_new_stack_is_empty(self):
self.assertEqual(len(self.stack), 0)
self.assertFalse(self.stack)
def test_push_increases_length(self):
self.stack.push(1)
self.assertEqual(len(self.stack), 1)
def test_pop_returns_last_pushed(self):
self.stack.push(1)
self.stack.push(2)
self.assertEqual(self.stack.pop(), 2)
self.assertEqual(len(self.stack), 1)
def test_peek_does_not_remove(self):
self.stack.push(1)
self.assertEqual(self.stack.peek(), 1)
self.assertEqual(len(self.stack), 1)
def test_pop_empty_raises(self):
with self.assertRaises(IndexError):
self.stack.pop()
def test_order_is_last_in_first_out(self):
for value in [1, 2, 3]:
self.stack.push(value)
self.assertEqual([self.stack.pop() for _ in range(3)], [3, 2, 1])What to test
| Case | Example |
|---|---|
| The normal path | net_price(100) |
| The boundaries | 0, 1, the maximum, the minimum |
| Empty input | "", [], {} |
| Invalid input | Negative, wrong type, None |
| Errors | Does it raise what it promises? |
| Known bugs | A test for every bug you fix |
import unittest
def split_name(full_name):
parts = full_name.strip().split()
if not parts:
raise ValueError("name cannot be empty")
if len(parts) == 1:
return parts[0], ""
return parts[0], parts[-1]
class TestSplitName(unittest.TestCase):
def test_two_parts(self):
self.assertEqual(split_name("Meera Nair"), ("Meera", "Nair"))
def test_three_parts(self):
self.assertEqual(split_name("Meera Sunita Nair"), ("Meera", "Nair"))
def test_single_name(self):
self.assertEqual(split_name("Meera"), ("Meera", ""))
def test_extra_whitespace(self):
self.assertEqual(split_name(" Meera Nair "), ("Meera", "Nair"))
def test_empty_raises(self):
for bad in ["", " ", "\n"]:
with self.subTest(value=bad):
with self.assertRaises(ValueError):
split_name(bad)subTest runs each case separately inside one test method, so all of them are reported rather than stopping at the first failure.
Table driven tests
import unittest
def grade(score):
if not 0 <= score <= 100:
raise ValueError("score must be between 0 and 100")
for threshold, letter in [(90, "A"), (80, "B"), (70, "C"), (40, "D")]:
if score >= threshold:
return letter
return "F"
class TestGrade(unittest.TestCase):
CASES = [
(100, "A"), (90, "A"), (89, "B"), (80, "B"),
(79, "C"), (70, "C"), (69, "D"), (40, "D"),
(39, "F"), (0, "F"),
]
def test_grades(self):
for score, expected in self.CASES:
with self.subTest(score=score):
self.assertEqual(grade(score), expected)
def test_out_of_range(self):
for score in [-1, 101]:
with self.subTest(score=score):
with self.assertRaises(ValueError):
grade(score)Note that the cases sit exactly on every boundary. Off by one errors live at boundaries, so that is where the tests belong.
Mocking
import unittest
from unittest.mock import Mock, patch
def fetch_user(client, user_id):
response = client.get(f"/users/{user_id}")
if response["status"] != 200:
raise LookupError(f"user {user_id} not found")
return response["data"]
class TestFetchUser(unittest.TestCase):
def test_success(self):
client = Mock()
client.get.return_value = {"status": 200, "data": {"name": "Meera"}}
result = fetch_user(client, 1)
self.assertEqual(result["name"], "Meera")
client.get.assert_called_once_with("/users/1")
def test_not_found(self):
client = Mock()
client.get.return_value = {"status": 404, "data": None}
with self.assertRaises(LookupError):
fetch_user(client, 99)import unittest
from unittest.mock import patch
from datetime import date
def greeting(name):
hour = __import__("datetime").datetime.now().hour
part = "morning" if hour < 12 else "afternoon"
return f"Good {part}, {name}"
class TestWithPatch(unittest.TestCase):
@patch("builtins.input", return_value="Meera")
def test_input(self, mock_input):
self.assertEqual(input("name? "), "Meera")
mock_input.assert_called_once()
def test_print(self):
with patch("builtins.print") as mock_print:
print("hello")
mock_print.assert_called_once_with("hello")Mocking replaces something slow, external or unpredictable - a network, a clock, a database, user input - with a stand in you control. Use it for the boundaries of your program, not for its own logic.
Skipping and expected failures
import unittest
import sys
class TestPlatform(unittest.TestCase):
@unittest.skip("not implemented yet")
def test_future_feature(self):
pass
@unittest.skipIf(sys.platform == "win32", "posix only")
def test_posix_paths(self):
pass
@unittest.skipUnless(sys.version_info >= (3, 10), "needs Python 3.10")
def test_match_statement(self):
pass
@unittest.expectedFailure
def test_known_bug(self):
self.assertEqual(1, 2)Reading a failure
FAIL: test_default_rate (test_pricing.TestNetPrice)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_pricing.py", line 8, in test_default_rate
self.assertEqual(net_price(100), 118.0)
AssertionError: 120.0 != 118.0
Ran 4 tests in 0.001s
FAILED (failures=1)- FAIL means an assertion was false: the code ran and gave the wrong answer.
- ERROR means an exception was raised: the code crashed.
- The assertion message shows both values, which usually identifies the bug immediately.
Layout
project/
notesapp/
__init__.py
pricing.py
storage.py
tests/
__init__.py
test_pricing.py
test_storage.py$ python -m unittest discover
$ python -m unittest discover -s tests -p "test_*.py" -vCoverage of behaviour, not lines
import unittest
def apply_discount(price, percent):
if percent < 0 or percent > 100:
raise ValueError("percent must be between 0 and 100")
return round(price * (1 - percent / 100), 2)
class TestDiscount(unittest.TestCase):
def test_typical(self):
self.assertEqual(apply_discount(100, 20), 80.0)
def test_no_discount(self):
self.assertEqual(apply_discount(100, 0), 100.0)
def test_full_discount(self):
self.assertEqual(apply_discount(100, 100), 0.0)
def test_rounding(self):
self.assertEqual(apply_discount(19.99, 15), 16.99)
def test_invalid_percent(self):
for percent in [-1, 101]:
with self.subTest(percent=percent):
with self.assertRaises(ValueError):
apply_discount(100, percent)Every line of apply_discount is covered by the first test alone. The other four exist because coverage measures lines executed, not cases considered.
Common mistakes
- Testing implementation details instead of behaviour, so every refactor breaks the tests.
- Writing tests that depend on each other or on execution order.
- Using
assertTrue(a == b)instead ofassertEqual. - Comparing floats with
assertEqual. - Leaving files, directories or state behind, so the second run behaves differently.
- Naming a method
check_something, which is never discovered. - Testing only the happy path.
Best practices
- One behaviour per test, and a name that states it:
test_pop_empty_raises. - Arrange, act, assert - in that order, visibly.
- Test boundaries, empty input and error cases.
- Use
setUpfor shared preparation and clean up intearDown. - Write a failing test for every bug before fixing it.
- Keep tests fast; mock anything slow or external.
Practice
- Write tests for a function converting temperatures, including negative and zero.
- Write tests for a
Queueclass covering empty, one item and many. - Use
subTestto check ten boundary values in one test method. - Mock
inputand test a function that prompts the user. - Find a bug in your own code, write a failing test for it, then fix it.
Conclusion
A test states what the code should do and fails loudly when it stops doing it. Name each one after the behaviour it checks, keep it independent, test the boundaries and errors, and add one for every bug you find.