Skip to Content
Course content

298: Mocking External Dependencies with unittest.mock

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

I've seen a lot of developers approach testing external dependencies by simply creating a "test" account on the service they're using—like a Stripe sandbox or a Mailgun test domain—and hitting the real API during their unit tests. On the surface, it feels more "authentic." You're testing the real plumbing, right? But in a professional CI/CD pipeline, this is a recipe for disaster.

import requests

class PaymentProcessor:
    def charge_customer(self, customer_id, amount):
        # Imagine this hits a real production-like API
        response = requests.post(
            "https://api.payments-gateway.com/v1/charge",
            json={"customer": customer_id, "amount": amount}
        )
        return response.json().get("status") == "success"

The cost of hitting the real wire

If you write a test that actually calls that charge_customer method, you've just turned your unit test into an integration test. Now, your test suite's success depends on the network being up, the API gateway not being throttled, and your API keys being correctly configured in the environment. If the payment gateway goes down for maintenance at 3:00 AM, your build pipeline fails, and your team gets paged—not because your code is broken, but because someone else's server is rebooting. It's slow, it's flaky, and frankly, it's annoying.

Replacing the dependency with a double

This is where unittest.mock comes in. Instead of letting the code actually travel across the internet, we "patch" the function or class that handles the network call. We replace it with a Mock object that looks like the real thing but exists entirely in memory. I prefer the patch decorator because it handles the cleanup for you, swapping the real object back in once the test finishes.

from unittest import TestCase
from unittest.mock import patch
from my_app import PaymentProcessor

class TestPaymentProcessor(TestCase):
    @patch('my_app.requests.post')
    def test_charge_customer_success(self, mock_post):
        # We tell the mock exactly what to return when it's called
        mock_post.return_value.json.return_value = {"status": "success"}
        
        processor = PaymentProcessor()
        result = processor.charge_customer("cust_123", 100)
        
        self.assertTrue(result)
        # We can also verify the code actually sent the right data
        mock_post.assert_called_once_with(
            "https://api.payments-gateway.com/v1/charge",
            json={"customer": "cust_123", "amount": 100}
        )

Controlling the chaos

The real power of mocking isn't just about speed; it's about testing the "untestable" scenarios. How do you test how your app behaves when the API returns a 500 Internal Server Error? Or a 429 Too Many Requests? Trying to force a third-party API to crash on command is nearly impossible. With a mock, it's a one-liner.

By setting the side_effect attribute of a mock, you can make it raise an exception or return different values on consecutive calls. This allows you to verify that your error-handling logic—the try/except blocks you spent hours writing—actually works as intended.

When to stop mocking

A word of caution: don't mock everything. If you mock your entire database layer and every single internal utility function, you're essentially testing that Python works, not that your logic works. I generally follow a rule of thumb: mock the boundaries of your system. Mock the API calls, the filesystem, and the clock. Keep the internal business logic real. If you find yourself mocking five different things just to get one test to run, that's usually a signal that your function is doing too much and needs to be broken down.




📋 Practical Task

Exercise: Simulating a Weather API Timeout

You are working on a system that sends emergency alerts based on weather data. The system uses a WeatherClient class that calls a remote API. You need to ensure that the system doesn't crash when the API times out; instead, it should return a fallback value of "Unknown".

Your task: Write a test case using unittest.mock.patch that simulates a requests.exceptions.Timeout exception being raised by the requests.get method. Verify that the get_current_temperature method handles this exception and returns "Unknown".

# code_to_test.py
import requests

class WeatherClient:
    def get_current_temperature(self, city):
        try:
            response = requests.get(f"https://api.weather.com/v1/{city}")
            response.raise_for_status()
            return response.json().get("temp")
        except (requests.exceptions.RequestException, requests.exceptions.Timeout):
            return "Unknown"

Implement the test class in a separate file, ensuring you correctly patch requests.get and use side_effect to trigger the timeout.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.