Testing What You Can't Reach
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:
- Hit the real API — slow, flaky, rate-limited, and changes over time
- Require authentication — credentials in CI is a headache
- 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
.jsonget 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.