Writing Resilient Python Unit Tests: Beyond the Basics

ArjanCodes////4 min read

Overview of Reliable Unit Testing

Unit testing validates the behavior of small, isolated code fragments, typically individual functions or methods. These tests serve as a critical safety net during refactoring, ensuring that modifications in one area don't inadvertently break unrelated logic. Beyond catching bugs, well-crafted tests act as a live specification of how your system should behave. While Python includes a built-in unittest module, the industry standard has shifted toward pytest due to its readable syntax and powerful feature set.

Prerequisites

To follow this guide, you should have a solid grasp of Python fundamentals, including classes and decorators. Familiarity with the pip package manager and basic command-line operations is necessary. You should also understand the basics of HTTP requests, as our examples involve simulating API interactions.

Key Libraries & Tools

  • pytest: A framework that simplifies writing small tests while scaling to support complex functional testing.
  • httpx: A next-generation HTTP client for Python used in our examples to fetch weather data.
  • unittest.mock: A standard library module that allows you to replace parts of your system under test with mock objects.

Code Walkthrough: Handling External Dependencies

Testing code that relies on external APIs, such as a WeatherService, is challenging because you cannot perform real HTTP requests during a unit test. You must replace the network call with a controlled response.

Writing Resilient Python Unit Tests: Beyond the Basics
How to Write Great Unit Tests in Python

Implementation with Monkey Patching

Monkey patching dynamically replaces a function at runtime. In the following example, we replace httpx.get with a fake version that returns pre-defined data.

import pytest
import httpx
from weather import WeatherService

def test_get_temperature_with_patch(monkeypatch):
    def fake_get(url, params=None):
        class FakeResponse:
            def raise_for_status(self):
                return None
            def json(self):
                return {"current": {"temp": 19}}
        return FakeResponse()

    monkeypatch.setattr(httpx, "get", fake_get)
    service = WeatherService(api_key="test_key")
    assert service.get_temperature("Amsterdam") == 19

Implementation with MagicMock

The MagicMock class from unittest.mock offers a cleaner alternative to manual fake classes. It allows you to define return values and verify how many times a method was called.

from unittest.mock import MagicMock, patch

def test_with_mock():
    mock_response = MagicMock()
    mock_response.json.return_value = {"current": {"temp": 25}}
    
    with patch("httpx.get", return_value=mock_response) as mock_get:
        service = WeatherService(api_key="test_key")
        temp = service.get_temperature("Utrecht")
        assert temp == 25
        mock_get.assert_called_once()

Syntax Notes: Pytest Features

  • Fixtures: Use the @pytest.fixture decorator to define reusable setup code. Passing the fixture name as an argument to a test function automatically injects the object.
  • Parametrization: Use @pytest.mark.parametrize to run the same test logic against multiple sets of data without duplicating code.
  • Exception Testing: Use with pytest.raises(ExceptionType): to verify that your code handles errors correctly.

Refactoring for Testability

Direct dependencies on libraries like httpx make testing rigid. By using Dependency Injection, you pass a client object into the WeatherService constructor. This allows you to swap the real HTTP client for a mock client during testing without ever touching monkey patches. Good design and testability go hand in hand; if a function is hard to test, it usually needs a better architectural approach.

Tips & Gotchas

  • Python Path: Ensure your pyproject.toml includes the correct pythonpath. Without it, pytest may fail to find your local modules.
  • Assert Quantity: Aim for one logical assertion per test to maintain clarity. If a test fails, you should know exactly why immediately.
  • Skip & XFail: Use pytest.mark.skip for temporary bypasses and pytest.mark.xfail for known bugs that are being tracked but not yet fixed.
Topic DensityMention share of the most discussed topics · 8 mentions across 4 distinct topics
pytest
38%· software
httpx
25%· software
Python
25%· languages
Squarespace
13%· software
End of Article
Source video
Writing Resilient Python Unit Tests: Beyond the Basics

How to Write Great Unit Tests in Python

Watch

ArjanCodes // 22:57

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