How to Write Unit Tests for Existing Python Code: A Comprehensive Guide
Overview
Writing unit tests for existing codebases is a common challenge for developers. Ideally,
Prerequisites
To follow this guide, you should have a baseline understanding of

Key Libraries & Tools
- pytest: The primary testing framework used for writing and running test cases with simple assert statements.
- pytest-cov: An extension that generates coverage reports to show which lines of code the tests actually exercise.
- Monkeypatch: A built-in pytestfixture that allows you to safely mock or override functions, attributes, and environment variables during testing.
Code Walkthrough
Testing Data Structures
We start with the simplest components: LineItem and Order. These are data-heavy and logic-light, making them ideal starting points.
from pay.order import LineItem
def test_line_item_total():
item = LineItem(name="Test", price=100, quantity=5)
assert item.total == 500
In this snippet, we initialize a LineItem and use a standard assert to check if the property total calculates correctly.
Handling Exceptions
When testing a pytest.raises as a context manager to catch expected exceptions.
import pytest
from pay.processor import PaymentProcessor
def test_invalid_api_key():
processor = PaymentProcessor(api_key="")
with pytest.raises(ValueError):
processor.charge("4242...", 12, 2024, 100)
This ensures the code fails gracefully when security requirements aren't met.
Mocking Global Inputs
The most difficult part of testing legacy code is dealing with input() calls and external dependencies. We use monkeypatch to simulate user keyboard input without stopping the test execution.
def test_pay_order(monkeypatch):
inputs = ["1234123412341234", "12", "2024"]
monkeypatch.setattr("builtins.input", lambda _: inputs.pop(0))
# ... call pay_order logic here ...
This replaces the standard input system with a lambda function that yields our predefined values one by one.
Syntax Notes
- Test Discovery: pytestautomatically finds files starting with
test_and functions starting withtest_. - Fixture Injection: By including
monkeypatchas an argument in your test function,pytestautomatically provides the tool for that specific test case. - Assertions: Unlike the standard library's
unittest,pytestrelies on plain Pythonassertstatements, making the code cleaner and more readable.
Practical Examples
Beyond point-of-sale systems, these techniques apply to any application where external API calls or user interactions are hard-coded into functions. For instance, if you have a script that fetches weather data, you can use monkeypatch to override the network request, returning a static JSON response instead of hitting a live server. This allows for fast, deterministic testing without an internet connection.
Tips & Gotchas
- API Keys: Never hard-code real API keys in your test files. Use environment variables or mock the validation check entirely to avoid security leaks.
- State Leakage: Be careful when patching global objects. pytest's
monkeypatchfixture automatically reverts changes after the test finishes, which is safer than manual patching. - Coverage Trap: 100% code coverage doesn't mean your code is bug-free; it just means every line was executed. Focus on testing edge cases like empty orders or dates in the past.