Testing with Unittest
Turn behavioral requirements into repeatable examples that detect meaningful regressions.
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.
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.
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.
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.
The ideas worth keeping.
- Encode requirements with explicit expected outcomes.
- Include boundaries and expected failure behavior.
- Keep fixtures independent and external effects temporary.
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.
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.
Explore a worked solution
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.
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.
Explore a worked solution
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.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
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.
[Process exited with code 0 - No output]
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.