The hardest part of testing an API client isn’t writing the assertions. It’s figuring out what to do about the network.

The Problem

You have code that fetches data from an external API. You want to test that your parsing logic, error handling, and data transformation work correctly. But you don’t want your tests to:

  1. Hit the real API — slow, flaky, rate-limited, and changes over time
  2. Require authentication — credentials in CI is a headache
  3. Depend on specific data — the test that passes today breaks tomorrow when the data changes

The Solution: Test the Seams

The trick is to identify where your code has seams — boundaries between the part that talks to the network and the part that processes the response.

A well-structured client has a clean separation:

fetch_json(url) → raw dict
parse_response(dict) → structured data
format_output(data) → user-facing string

Each of these is independently testable. And only the first one needs the network.

Mock the Boundary

For fetch_json, mock urlopen and return pre-built JSON. Now you can test:

  • Does the URL get constructed correctly?
  • Does .json get appended properly?
  • Are query parameters in the right place?
  • Do HTTP errors (404, 429, 403) raise the right exceptions?

For the parsers and formatters — they’re pure functions. Give them input, check the output. No mocking needed.

What This Looks Like in Practice

Instead of testing “call the API and check the result” (integration test), you write:

  • 10 tests for URL construction and error handling (mocked HTTP)
  • 15 tests for parsing raw dicts into typed objects (no mocking)
  • 20 tests for formatting objects into markdown (no mocking)
  • 10 tests for cache behavior (temp directories)

The result: 55 tests that run in under a second, never touch the network, and cover every code path.

The One Integration Test You Still Need

All of this unit testing doesn’t prove the system works end-to-end. You still need at least one integration test that hits the real API (or a containerized version of it). But that test can be:

  • Marked as slow / optional
  • Run separately from the main suite
  • Triggered on deploy, not on every commit

The unit tests give you confidence that the logic is correct. The integration test gives you confidence that the API hasn’t changed. Different purposes, different cadences.

The Principle

Test the logic, mock the I/O. Most bugs live in parsing, formatting, and edge case handling — not in HTTP libraries. Put your testing energy where the bugs are.