Mocking External Services in Pytest

Scenario: Mocking an API Call in Pytest

Let’s assume we have a function get_weather_data() that fetches weather details from an external API using the requests library. Since requests.get() makes an actual network call, we need to mock it in our tests.

# The function to be tested
import requests

def get_weather_data(city):
    """Fetches weather data for a given city from an external API."""
    url = f"https://api.weather.com/v3/weather/{city}"
    response = requests.get(url)
    
    if response.status_code == 200:
        return response.json()
    else:
        return {"error": "Unable to fetch data"}

Using Pytest’s monkeypatch to Mock API Calls

import pytest

def mock_requests_get(*args, **kwargs):
    """Mock function to replace requests.get"""
    class MockResponse:
        def __init__(self, json_data, status_code):
            self.json_data = json_data
            self.status_code = status_code

        def json(self):
            return self.json_data

    # Simulate a successful response
    if "weather" in args[0]:
        return MockResponse({"temperature": 25, "condition": "Sunny"}, 200)
    return MockResponse({"error": "Invalid request"}, 400)

def test_get_weather_data(monkeypatch):
    """Test function using monkeypatch to mock requests.get"""
    monkeypatch.setattr(requests, "get", mock_requests_get)

    response = get_weather_data("NewYork")
    assert response == {"temperature": 25, "condition": "Sunny"}

Breakdown:

  • We define mock_requests_get() to return a fake response instead of making an actual API call.
  • We use monkeypatch.setattr(requests, "get", mock_requests_get) to override requests.get with our mock function.
  • The test then verifies that the function correctly processes the mocked response.

Using unittest.mock for More Flexibility

The unittest.mock library provides a more powerful way to mock external dependencies.

Key Advantages:

  • patch("requests.get") dynamically replaces requests.get with a mock.
  • mock_get.return_value allows us to control the return values without defining a separate mock function.
  • The approach is reusable and works well with more complex interactions.
from unittest.mock import patch
import requests

@patch("requests.get")
def test_get_weather_data_with_mock(mock_get):
    """Test function using unittest.mock to patch requests.get"""
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {"temperature": 30, "condition": "Cloudy"}

    response = get_weather_data("London")
    assert response == {"temperature": 30, "condition": "Cloudy"}

Handling Edge Cases in Mocking

Simulating API Failures

@patch("requests.get")
def test_get_weather_data_failure(mock_get):
    mock_get.return_value.status_code = 500
    mock_get.return_value.json.return_value = {"error": "Server Error"}

    response = get_weather_data("Paris")
    assert response == {"error": "Unable to fetch data"}

Simulating Timeouts

@patch("requests.get")
def test_get_weather_data_timeout(mock_get):
    mock_get.side_effect = requests.exceptions.Timeout

    with pytest.raises(requests.exceptions.Timeout):
        get_weather_data("Berlin")

Simulating Different Response Structures

# APIs often return different data structures under different conditions. Mocking lets you test all possible variations.
@patch("requests.get")
def test_get_weather_data_invalid_json(mock_get):
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.side_effect = ValueError("Invalid JSON")

    response = get_weather_data("Tokyo")
    assert response == {"error": "Unable to fetch data"}

Best Practices for Mocking in Pytest

Keep Mocks as Close to Reality as Possible – Ensure that mock responses resemble real API responses.
Test Both Success & Failure Cases – Always test edge cases, including timeouts, incorrect data, and server errors.
Use unittest.mock.patch for Cleaner Code – It’s more concise and integrates well with pytest.
Avoid Over-Mocking – Mocking too much can lead to tests that don’t truly reflect real-world behavior.

Advanced Mocking Techniques

1️⃣ Using responses Library for HTTP Mocking

# Instead of manually mocking requests.get, you can use the responses library to mock entire HTTP requests.
import responses

@responses.activate
def test_get_weather_data_with_responses():
    responses.add(
        responses.GET, 
        "https://api.weather.com/v3/weather/NewYork",
        json={"temperature": 20, "condition": "Windy"}, 
        status=200
    )

    response = get_weather_data("NewYork")
    assert response == {"temperature": 20, "condition": "Windy"}

2️⃣ Using pytest-mock for Cleaner Mocking

# The pytest-mock plugin simplifies mocking in Pytest.
def test_get_weather_data_with_pytest_mock(mocker):
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {"temperature": 28, "condition": "Rainy"}

    response = get_weather_data("Mumbai")
    assert response == {"temperature": 28, "condition": "Rainy"}

📚 Further Reading & Resources

🔗 Popular Resources

🎯 Underrated Resources

📌 Popular GitHub Repositories

🤖 AI Tools for Improved Testing

  • CodiumAI – AI-powered test case generation
  • ChatGPT API – Generate automated test mocks
  • DeepCode – AI-driven static code analysis
Search

Table of Contents

You may also like to read