Skip to Content
Course content

299: Patching Objects in Tests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Alright, let's look at a real-world headache: external dependencies. Imagine we've written a small service that fetches the current temperature from a third-party API. It's simple code, but it's a nightmare to test because we can't control the weather, and we certainly don't want our CI/CD pipeline failing just because a server in Virginia decided to go offline for five minutes.

# weather_service.py
import requests

def get_temperature(city):
    # In a real app, you'd have an API key here
    response = requests.get(f"https://api.weather.com/v1/{city}")
    data = response.json()
    return data["temp"]

The "It Works on My Machine" API Trap

My first instinct when testing this is to just call the function. But look what happens when I write a basic test for it:

# test_weather.py
from weather_service import get_temperature

def test_get_temperature():
    temp = get_temperature("London")
    assert temp == 15

This is a fragile test. If the actual temperature in London is 16, the test fails. If the API is down, the test crashes. This isn't testing my logic; it's testing the internet. We need a way to "trick" get_temperature into thinking it's talking to the API, while we actually provide a canned response.

Swapping the Real Deal for a Double

This is where unittest.mock.patch comes in. The goal is to replace the requests.get function with a "Mock" object that we control. Let's try to just wrap the test in a patch decorator:

from unittest.mock import patch
from weather_service import get_temperature

@patch('requests.get')
def test_get_temperature(mock_get):
    # I'll tell the mock what to return when it's called
    mock_get.return_value.json.return_value = {"temp": 15}
    
    temp = get_temperature("London")
    
    assert temp == 15
    mock_get.assert_called_once_with("https://api.weather.com/v1/London")

I ran this, and... it failed. Specifically, the real requests.get was still being called. Why? This is the most common stumbling block with patching in Python. I patched 'requests.get', but weather_service.py had already imported requests when the program started. It's holding onto its own reference to the real library.

Where Exactly Are We Patching?

The rule of thumb in Python is: patch where the object is looked up, not where it is defined.

Since get_temperature is inside weather_service.py, that's where the lookup happens. I don't want to patch the global requests library; I want to patch the version of requests that exists inside the weather_service module. Let's adjust the path:

# Change this:
@patch('requests.get')

# To this:
@patch('weather_service.requests.get')

Now, when get_temperature calls requests.get, it's not looking at the actual library—it's looking at the mock I've injected into that specific module. I ran it again, and it passed instantly. No network calls, no API keys, and total control over the data.

One last thing: notice how I used mock_get.return_value.json.return_value? That looks ridiculous, I know. But remember that requests.get() returns a Response object, and that object has a .json() method, which in turn returns a dictionary. To mock a chain of calls, you have to chain .return_value for every step of the process.




📋 Practical Task

Simulating a Flaky Payment Gateway

You are testing a PaymentProcessor class that uses a library called stripe_client to charge customers. The charge method in stripe_client can either return a success dictionary or raise a ConnectionError if the gateway is down.

Your Task: Write a test for the process_payment function that simulates a gateway failure. You must patch the stripe_client.charge method so that instead of making a real network call, it raises a ConnectionError. Verify that your process_payment function catches this error and returns the string "Gateway Timeout".

Starter Code:

# payment_system.py
import stripe_client

def process_payment(amount):
    try:
        stripe_client.charge(amount)
        return "Success"
    except ConnectionError:
        return "Gateway Timeout"

# Your test file:
# Use unittest.mock.patch to make stripe_client.charge raise ConnectionError
Rating
0 0

There are no comments for now.

to be the first to leave a comment.