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. 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. ```python 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. ```python 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.
Squarespace
Software
- Aug 15, 2025
- Oct 4, 2022