How to Write Unit Tests for Existing Python Code: A Comprehensive Guide

ArjanCodes////4 min read

Overview

Writing unit tests for existing codebases is a common challenge for developers. Ideally, Test-Driven Development (TDD) ensures code is born with tests, but real-world projects often contain legacy logic lacking proper validation. This tutorial demonstrates how to add robust testing to an existing Python system without altering the original source code immediately. By using pytest, we can secure business logic, verify data structures, and prepare the ground for future refactoring. Adding tests to a point-of-sale system helps identify brittle dependencies and ensures that core features like price calculation and payment validation remain stable during updates.

Prerequisites

To follow this guide, you should have a baseline understanding of Python syntax, including classes, methods, and decorators. Familiarity with the terminal and basic testing concepts like assertions is necessary. You will need a Python environment with pytest and pytest-cov installed to run the tests and generate coverage reports.

How to Write Unit Tests for Existing Python Code: A Comprehensive Guide
How To Write Unit Tests For Existing Python Code // Part 1 of 2

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 pytest fixture 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 PaymentProcessor, we must verify that invalid inputs trigger the correct errors. We use 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: pytest automatically finds files starting with test_ and functions starting with test_.
  • Fixture Injection: By including monkeypatch as an argument in your test function, pytest automatically provides the tool for that specific test case.
  • Assertions: Unlike the standard library's unittest, pytest relies on plain Python assert statements, 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 monkeypatch fixture 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.
Topic DensityMention share of the most discussed topics · 17 mentions across 9 distinct topics
pytest
47%· products
pytest-cov
12%· products
GitHub
6%· products
Luhn Checksum
6%· concepts
Other topics
24%
End of Article
Source video
How to Write Unit Tests for Existing Python Code: A Comprehensive Guide

How To Write Unit Tests For Existing Python Code // Part 1 of 2

Watch

ArjanCodes // 25:07

On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
27.3%3
Python
18.2%2
Python
18.2%2
4 min read0%
4 min read