Should I test that?

Should unit tests make real API calls?

Verdict

Test it differently

No, unit tests should not call a real third-party API: replace it with a fake HTTP layer that returns recorded responses, and check the live API in a scheduled run outside the build, plus an alert on production responses that your code cannot parse.

Why

Test it differently: keep real API calls out of unit tests, and check the live API with a nightly run and a production alert. The typical case is a backend module that calls a third-party HTTP API, with a real call in the unit test to prove the code still works against the live service. Blast radius is users, because a broken integration shows customers missing data. Change frequency is rarely, because a versioned public API changes the fields you read a few times a year. Detectability is eventually, because a renamed field reaches your code as null and shows a blank that nobody reports for days, and Reversibility is with-effort, because records saved with that blank need a repair script. Test cost is heavy, because every run needs a sandbox key and the network, and fails on the provider's rate limits and outages, so rule R9 gives Test it differently.

When the decision changes
WhenDecisionWhy
The failure to catch is in your own code, such as parsing a response or handling a 429Test: unit tests with a fake HTTP layer that returns one recorded response for each outcome the code handlesTest cost falls to moderate, because a recorded response reproduces that parsing or 429 failure without the network, and Change frequency rises to regularly, because your requests change with features
The API charges customers' cards, as a payment provider doesTest mandatory: fake-server tests for each outcome, including a decline and a timeout, plus a run in the provider's test mode before each releaseBlast radius rises to money and Reversibility to costly, because a double charge ends in a refund
The API is a service that your team owns, and CI can start it in a containerTest: call the real service from integration tests inside the build, and keep a fake in unit testsTest cost falls to moderate, because the service runs in the build with no rate limit, and Change frequency rises to regularly, because your team changes it with features
The code only reads a value for display, such as a delivery estimate, and a change in the provider's responses shows an error state that users reportDo not add a live check; keep the fake HTTP layer tests of the parser, and update the parser and its recorded response when users report the errorDetectability moves to same-day and Reversibility to trivial, because a read stores nothing

What breaks if you don't test

Without any live check, recorded fixtures describe the API as it was on the day you recorded them. The provider moves a field, every test stays green, and the first signal is a customer who sees an empty field a week later, with the blank saved in every record created since.

What you lose if you over-test

A real call makes a unit test depend on the network, the provider's uptime, its rate limit, and sandbox data that other runs change. Martin Fowler's article on non-determinism calls tests that fail at random useless, because the team learns to rerun them instead of reading them.

What to do instead

  1. In unit tests, replace the HTTP layer with a fake such as responses, which raises ConnectionError for any request without a registered response.
  2. Record the fixtures from the provider's sandbox, including each error your code handles.
  3. Mark the few tests that call the sandbox with a pytest marker, exclude them from the build with -m "not live", and run them nightly with the sandbox key. The nightly run is a contract test that fails when the fixtures are out of date.
  4. In production, check each response for the fields your code reads, and alert when one is missing.

When the answer changes

  • The API moves money or returns data that only one customer may see.
  • Your team owns the service, and CI can start it.
  • The call only reads a value for display, and users report a failure the same day.

Real incident + Code example

The failing test we took for a rate limit

On a delivery-scheduling product I worked on, the unit tests for the address lookup client called the provider's sandbox. After CI started to run four jobs in parallel, about one build in ten failed with HTTP 429, and the team got used to rerunning red builds. Then the sandbox started to return the house number in a separate field, and one test failed on every run. We took it for another 429 and skipped it. The change reached production eight days later, and the booking form saved addresses without house numbers for two days before drivers complained. We moved the tests to recorded responses and kept one live test for the nightly job:

import json
import pytest
import responses
from address_client import lookup

RECORDED = json.load(open("fixtures/lookup_flat.json"))  # recorded from the sandbox

@responses.activate  # an unregistered request raises ConnectionError
def test_lookup_keeps_house_number():
    responses.get("https://api.geo.example/v2/lookup", json=RECORDED)
    address = lookup("SW1A 1AA", "10")
    assert address.house_number == "10"

@pytest.mark.live  # nightly job only: pytest -m live, with the sandbox key
def test_sandbox_still_matches_recording():
    address = lookup("SW1A 1AA", "10", base_url="https://sandbox.geo.example")
    assert address.house_number == "10"

FAQ

Should integration tests call external services?

Integration tests should call the services you run, such as your database, and replace third-party services with a fake server that returns recorded responses. Check the real third-party service in a scheduled run, so a provider outage does not fail your build.

Should unit tests mock all external services?

Yes, a unit test should replace every service it reaches over the network. Replace it at the HTTP boundary with recorded responses, not by mocking your own client class, so the test still runs your parsing code.

How do I know my recorded responses still match the real API?

Recorded responses still match the real API while a scheduled contract test sends the same requests to the provider's sandbox and gets the same fields back. Record the fixtures again when that test fails or the provider announces a new version.