Mocking External Services in API Tests

Mocking external services in API tests — FastAPI Backend Development.

Focus: mocking external services in api tests

Sponsored

Your API calls a third-party payment gateway, a weather service, or a recommendation engine. In test, that service is either down, paid, rate-limited, or returns data that changes by the second. If you let real external calls run during your test suite, your tests become slow, flaky, and impossible to trust. This lesson shows you how to mock external services in API tests — the technique that isolates your code from the network and makes your suite fast, deterministic, and reliable.

The problem this lesson solves

Every non-trivial FastAPI backend depends on external services: payment processors, email providers, OCR endpoints, or internal microservices. When a test hits those services for real, you inherit a pile of problems:

  • Flakiness — the service hiccups or returns a slightly different response, and a test that passed yesterday fails today.
  • Slowness — every HTTP round-trip adds 100-1000ms. A suite of 200 tests that each make a network call takes minutes instead of seconds.
  • Cost and rate limits — paid APIs (Twilio, AWS, Stripe) charge you or throttle you, and your CI runs can trigger billing surprises.
  • Coupling — your test isn't testing your code; it's testing both your code and the third-party service. If their API contract changes (or breaks), your suite breaks even though you changed nothing.
  • Security / state leakage — you might accidentally write to a production database or trigger real emails during a test run.

The result: a suite that is slow, unpredictable, and screams so often that developers start to ignore it. Once a test is ignored, it is worse than no test at all.

Mocking external services in API tests is the solution. By replacing the network call with a controlled fake, you gain total control over inputs, outputs, and error paths. Your tests become hermetically sealed, deterministic, and run in milliseconds — and they test exactly what you wrote, nothing more.

Core concept / mental model

Think of your FastAPI application as a puppet. External services are the strings that move it. During production, the strings are connected and impulses flow. In a test, you want to hold those strings in your own hands and decide what each pull will do — that's the essence of mocking.

  • Mock — a stand-in object that mimics an external object's interface. You control its return values, side effects, and what methods are called.
  • Patch — the act of replacing (or 'monkey-patching') a real attribute or function with a mock at runtime, and restoring it afterwards.
  • Fixture — a pytest mechanism that provides a reusable setup for tests, often used to install mocks for the duration of a test.

The key is the dependency inversion principle that FastAPI encourages. You never call a global function directly inside your endpoint — you declare a dependency. That dependency is an interface (or an object) that you can replace with a mock in your tests. FastAPI's Depends() lets you swap out real implementations with test doubles cleanly.

Where to patch matters. You must patch where the function is looked up, not where it is defined. In plain words, if your endpoint imports requests.get and calls it, you need to patch app.main.requests.get — not requests.get. This is the single most common source of confusion when mocking.

Immutability during test. Once a mock is installed, it stays for the duration of a test (or fixture). Your code sees only the mock. No real network, no real side effects.

How it works step by step

  1. Identify the external call. Find where your endpoint reaches out — a requests.get call, a database client, an external library like stripe or httpx.
  2. Declare a dependency for the call. Instead of hardcoding the call inside the endpoint, wrap it in a function or an async function that is injected via Depends(). This makes the call swappable.
  3. Write a mock for that dependency. Use Python's unittest.mock.Mock or AsyncMock to create an object with the same interface. You control the return value, raising exceptions, etc.
  4. Override the dependency in tests. FastAPI has a built-in app.dependency_overrides dictionary. You set the original dependency to your mock, and it replaces the real one for every request that uses it.
  5. Use fixtures to manage lifecycle. In pytest, use a fixture to set up and tear down the overrides, so each test starts clean.
  6. Assert that the mock was called with the right arguments. This turns a generic test into a precise contract test.

The key is layer-by-layer: you mock at the boundary of your application, never deep inside. If your endpoint calls payments.charge(), you mock payments.charge, not the internals of that function (unless you need to).

Hands-on walkthrough

Let's build a small FastAPI app that calls a weather service, and then write tests that mock it.

Step 1: Create the app with a dependency.

# app/main.py
from fastapi import FastAPI, Depends, HTTPException
from services.weather import WeatherClient, weather_client

app = FastAPI()

def get_weather_client() -> WeatherClient:
    return weather_client

@app.get("/weather/{city}")
async def get_weather(city: str, client: WeatherClient = Depends(get_weather_client)):
    temp = client.get_temp(city)
    if temp is None:
        raise HTTPException(status_code=503, detail="Service unavailable")
    return {"city": city, "temp": temp}

Step 2: The real client (for production).

# services/weather.py
import httpx

class WeatherClient:
    BASE_URL = "https://api.weather.example"

    def get_temp(self, city: str) -> float | None:
        try:
            response = httpx.get(f"{self.BASE_URL}/{city}")
            response.raise_for_status()
            return response.json()["temp"]
        except (httpx.RequestError, KeyError):
            return None

weather_client = WeatherClient()

Step 3: Write a test with dependency override.

# tests/test_weather.py
from fastapi.testclient import TestClient
from unittest.mock import Mock
import pytest
from app.main import app, get_weather_client

@pytest.fixture
def mock_weather_client():
    mock = Mock()
    app.dependency_overrides[get_weather_client] = lambda: mock
    yield mock
    app.dependency_overrides.clear()

def test_get_weather_success(mock_weather_client):
    mock_weather_client.get_temp.return_value = 20.5
    client = TestClient(app)

    response = client.get("/weather/London")

    assert response.status_code == 200
    assert response.json() == {"city": "London", "temp": 20.5}
    mock_weather_client.get_temp.assert_called_once_with("London")

def test_get_weather_failure(mock_weather_client):
    mock_weather_client.get_temp.return_value = None
    client = TestClient(app)

    response = client.get("/weather/London")

    assert response.status_code == 503

Run with pytest — you'll see 2 tests pass in a blink, with zero network calls.

Step 4: Patch a raw external call (alternative). If you don't use dependency injection, you can patch the module attribute directly:

from unittest.mock import patch

@patch("app.main.weather_client")
def test_get_weather(mock_client):
    mock_client.get_temp.return_value = 10.0
    # ... rest of test

But note the path — you patch where it's used, not where it was created.

Step 5: Mock an async external call. If your client uses await (async), use AsyncMock and asyncio:

from unittest.mock import AsyncMock

@pytest.fixture
def mock_async_client():
    mock = AsyncMock()
    mock.get_temp.return_value = 25.0
    app.dependency_overrides[get_weather_client] = lambda: mock
    yield mock
    app.dependency_overrides.clear()

# Then in the endpoint, use `await client.get_temp(city)`

Expected output when running pytest:

collected 2 items

test_weather.py ..                                                                  [100%]

2 passed in 0.23s

Compare options / when to choose what

You have several tools to mock external services in API tests. Here’s a quick comparison:

Technique Use it when Pros Cons
FastAPI dependency_overrides Your code uses FastAPI dependencies (recommended design) Clean, explicit, no magic; integrates with test client; per-test control Requires discipline to structure code this way
unittest.mock.patch You want to monkey-patch a function or attribute anywhere Flexible, works on any attribute; simple for one-off Fragile if you patch wrong path; can hide structure
responses library You call requests/httpx directly inside endpoints Intercepts actual HTTP calls; returns realistic responses Adds a dependency; still couples to HTTP layer
respx (for httpx) You use httpx and want explicit HTTP mocking Async-friendly; works with AsyncClient Separate library; extra learning
Local fake server (e.g., pytest-httpx) You want to test full HTTP behavior without network Most realistic Slowest; requires server lifecycle

When to choose what?

  • If you can refactor to use dependencies, choose dependency_overrides — it's the most maintainable and testable approach.
  • If you are stuck with legacy code that calls global functions, use patch.
  • If you need to simulate many HTTP endpoints, the responses or respx libraries give you a clean API.
  • If you must test end-to-end behavior of a service (rare), spin up a local mock server.

Troubleshooting & edge cases

  • Patch at the wrong location — You patched requests.get but your endpoint imported from requests import get and calls get(). The mock never applies. Solution: patch the name in the module where it's used: app.main.get or app.main.requests.get.
  • Async mock not awaited — You used Mock() for an async function, and your test fails with TypeError: object NoneType can't be used in 'await' expression. Fix: use AsyncMock() for async functions.
  • Return value mutationmock.method.return_value is a single object shared across calls. If you need different values per call, use side_effect with a list or a callable.
  • Fixture cleanup — If you forget to clear app.dependency_overrides, overrides leak across tests, causing weird behavior. Always use a fixture that yields then clears.
  • Patching inside async tests — If you use asyncio.run or pytest-asyncio, patch using AsyncMock and ensure you patch before the endpoint runs.
  • Import errors with monkey-patch — You might patch app.main.weather_client but the code imports the same object from another module. In that case, patch the attribute in both modules or restructure.

What you learned & what's next

We covered the core idea behind mocking external services in API tests: isolating your code from the network to gain speed, determinism, and control. You learned to use FastAPI's dependency overrides and unittest.mock to create reliable test doubles, you practiced with synchronous and async clients, and you compared different mocking strategies.

Now you're ready to move to the next lesson in the FastAPI Backend Development track: Integration Testing Your API — where you'll test your app against a real database and external contracts, while still keeping your tests fast and focused. That's a natural next step because you'll combine the isolation techniques here with transaction rollbacks and fixtures.

Practice recap

Write a new endpoint in your existing FastAPI app that calls a mockable external service (e.g., a currency converter). Add a dependency for the client, then write three tests using dependency_overrides: a success case, a case where the service returns None, and a case where the response is invalid JSON. Run pytest and confirm all tests pass with no network activity — try adding a print inside the real client to prove it never runs.

Common mistakes

  • Patching the wrong module path — always patch where the function is called (e.g., app.main.client.get), not where it's defined in the service module.
  • Using Mock() instead of AsyncMock() for async endpoints — the test fails with 'object can't be used in await expression'.
  • Forgetting to clear app.dependency_overrides after each test, causing overrides to leak into later tests and make them fail unexpectedly.
  • Mocking the entire external library (stripe) instead of just the functions you call — this makes tests brittle when the library changes.
  • Testing only the happy path — mocking only successful responses and never a timeout or 500 error, leaving error-handling code untested.

Variations

  1. Use the responses library to mock requests calls at the HTTP layer, giving you rich response control (status codes, headers, payloads) without touching your code structure.
  2. Use respx for mocking httpx calls specifically — it's async-compatible and works smoothly with FastAPI's AsyncClient.
  3. Spin up a lightweight local mock server using pytest-httpx or uvicorn to simulate an external service if you need to test full request/response behavior without real network access.

Real-world use cases

  • Testing a payment endpoint that calls Stripe — mock stripe.Charge.create to return a fake charge ID and assert your app handles success and decline paths.
  • Testing a login endpoint that validates tokens via an external auth service — mock the verification call to return a known user for deterministic credentials.
  • Testing a recommendation endpoint that queries a remote ML service — mock the service response to test your app's caching and error fallback logic reliably.

Key takeaways

  • Mocking isolates your tests from external dependencies — they run fast, deterministically, and never break because a third-party service is down.
  • Always patch where the external function is looked up (your module's namespace), not where it's defined.
  • Prefer FastAPI's dependency_overrides for mocking because it promotes clean, testable code architecture.
  • Use AsyncMock for async functions; Mock only works for synchronous calls.
  • Clean up overrides in a fixture to avoid state leakage between tests.
  • Duplicate real-world success and failure responses (timeouts, 500s) to cover your error-handling paths.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.