CHAPTER 33 · INTERMEDIATE4 min read

Testing with Unittest

Turn behavioral requirements into repeatable examples that detect meaningful regressions.

Python 3.12+Free to readModule 8 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Write independent unittest test cases for observable behavior.
  • Cover boundaries and expected exceptions.
  • Isolate filesystem effects and run test discovery.

01Test the behavior callers rely on

A useful test states a requirement and supplies evidence that the program meets it. For a shipping rule, test an ordinary basket, the free-shipping threshold, and invalid input. Avoid merely repeating the implementation in the expected-value calculation; that can reproduce the same mistake on both sides.

unittest provides TestCase subclasses and methods whose names begin with test. Inside a test, arrange inputs, call the behavior, and compare the observable result with an independently chosen expectation. Use descriptive test names so a failure explains the broken rule before anyone opens the source.

test_shipping.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02Test boundaries and failure contracts

A boundary is a place where the rule changes: zero items, the maximum quantity, an empty file, or a deadline exactly equal to now. Values immediately below, at, and above a threshold often reveal mistakes that a comfortable middle value misses. Choose cases from the specification, not from a desire to execute every line once.

assertRaises verifies that a particular exception is raised. Put only the operation expected to fail inside its with block, otherwise an earlier setup failure can make the test pass accidentally. Use subTest to label related inputs while preserving separate failure reports. Avoid depending on the exact text of an incidental system error.

test_boundaries.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Make tests independent and temporary

A test should pass alone and in a different order from its neighbors. Do not depend on a previous test creating a file or populating a global list. setUp prepares fresh fixtures for each test; addCleanup can register cleanup immediately after a resource is acquired, including when later setup fails.

Use TemporaryDirectory for filesystem scenarios and pass the path into the code under test. For time-sensitive behavior, pass a fixed clock or explicit now. Prefer exercising a small real filesystem round trip over mocking every Path method when persistence behavior is what matters. Mock a collaborator when its external effect is outside the behavior being verified.

test_file_roundtrip.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Run and interpret the suite

In a normal project, save tests under names such as tests/test_shipping.py and run python -m unittest discover -s tests -v from the project root. A failure means an assertion did not hold; an error means the test could not complete because another exception occurred. Both require investigation rather than blindly changing the expectation.

When fixing a reported bug, add the smallest case that demonstrates the wrong behavior, confirm that it fails for the right reason, and then fix the code. Keep tests focused on supported results and effects, so renaming a private helper does not force unrelated test rewrites. A passing suite is evidence for the scenarios you encoded, not a proof that all possible inputs work.

LET IT CLICK

The ideas worth keeping.

  • Encode requirements with explicit expected outcomes.
  • Include boundaries and expected failure behavior.
  • Keep fixtures independent and external effects temporary.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Test a normalization contract

Implement normalize_title and a unittest suite. Trim titles, reject blank text, and cover whitespace around a normal title plus the empty boundary.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
import unittestdef normalize_title(text):    title = text.strip()    if not title:        raise ValueError("title cannot be blank")    return titleclass TitleTests(unittest.TestCase):    def test_trims_outer_whitespace(self):        self.assertEqual(normalize_title("  Learn files  "), "Learn files")    def test_blank_inputs_are_rejected(self):        for text in ("", " ", "\n"):            with self.subTest(text=repr(text)):                with self.assertRaises(ValueError):                    normalize_title(text)if __name__ == "__main__":    unittest.main()

The expected title is explicit rather than computed by repeating strip in the assertion. Separate invalid cases document the nonblank rule.

02

Test a missing-file policy

Write load_optional(path), returning empty text only for a missing file. Test both missing and existing empty files in fresh temporary directories.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from pathlib import Pathfrom tempfile import TemporaryDirectoryimport unittestdef load_optional(path):    try:        return path.read_text(encoding="utf-8")    except FileNotFoundError:        return ""class OptionalFileTests(unittest.TestCase):    def test_missing_file_is_empty(self):        with TemporaryDirectory() as folder:            self.assertEqual(load_optional(Path(folder) / "missing.txt"), "")    def test_existing_empty_file_is_supported(self):        with TemporaryDirectory() as folder:            path = Path(folder) / "empty.txt"            path.write_text("", encoding="utf-8")            self.assertEqual(load_optional(path), "")if __name__ == "__main__":    unittest.main()

Each test owns its data. Only FileNotFoundError is converted to the optional-file default, so unrelated access and decoding failures remain visible.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why test a free-shipping threshold at 4999, 5000, and 5001 cents?
02What belongs inside an assertRaises block?
03A test passes only when another test runs first. What is the most useful response?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Test a normalization contract

Implement normalize_title and a unittest suite. Trim titles, reject blank text, and cover whitespace around a normal title plus the empty boundary.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.