External HTTP calls are the most common source of flaky integration tests. The API is slow, rate-limited, or just unreachable in CI — and suddenly half your test suite is red for reasons unrelated to your code.
The standard fix is responses or httpretty: hand-written stubs that you maintain forever. The moment the real API adds a field, your stubs drift silently.
APIGhost reads your OpenAPI spec and starts a real HTTP server with realistic fake data. The stubs are generated from the spec, so they update when the spec updates. No maintained JSON blobs.
# Documented pip install path (APIGhost is not on public PyPI;
# its self-hosted index is currently unavailable)
pip install git+https://github.com/Coding-Dev-Tools/apighost.git
# macOS Homebrew
brew tap Coding-Dev-Tools/homebrew-tap
brew install apighost
APIGhost is not on public PyPI. Use one of the commands above.
Start the mock server as a session-scoped fixture so it launches once and all tests share it.
import subprocess
import time
import pytest
import requests
@pytest.fixture(scope="session", autouse=True)
def mock_api(tmp_path_factory):
port = 18080
proc = subprocess.Popen(
["apighost", "serve", "openapi.yaml", "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Wait for the server to accept connections
for _ in range(20):
try:
requests.get(f"http://localhost:{port}/", timeout=0.5)
break
except Exception:
time.sleep(0.25)
yield f"http://localhost:{port}"
proc.terminate()
def test_users_returns_list(mock_api):
resp = requests.get(f"{mock_api}/users")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
assert "email" in data[0] # faker fills property-name hints automatically
No stubs. No patching. Real HTTP, real JSON, zero external dependencies.
Faker randomness is great for smoke tests but breaks assertions on exact values. Record once, replay forever.
@pytest.fixture(scope="session", autouse=True)
def mock_api(tmp_path_factory):
cassette = "tests/fixtures/cassette.json"
port = 18080
proc = subprocess.Popen(
["apighost", "replay", cassette, "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# ... same wait loop as above ...
yield f"http://localhost:{port}"
proc.terminate()
Generate the cassette once against a real (or faker-based) server:
apighost record openapi.yaml --output tests/fixtures/cassette.json
Check the cassette into source control. Tests now run identically on every machine and every CI run.
APIGhost ships named scenario presets. To test what your code does when the API returns 500s, pass a fixture parameter instead of editing JSON:
@pytest.fixture
def mock_api_error():
port = 18081
proc = subprocess.Popen(
["apighost", "serve", "openapi.yaml", "--port", str(port), "--scenario", "error"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
for _ in range(20):
try:
requests.get(f"http://localhost:{port}/", timeout=0.5)
break
except Exception:
time.sleep(0.25)
yield f"http://localhost:{port}"
proc.terminate()
def test_client_handles_500_gracefully(mock_api_error):
resp = requests.get(f"{mock_api_error}/orders")
assert resp.status_code == 500
# Assert your retry / fallback logic kicks in
No fixture editing. Flip --scenario error on and your entire error-path test suite runs against server-level 500 responses.
APIGhost runs anywhere Python 3.10+ is available. Add it to your CI workflow alongside your test runner:
- name: Install test deps
run: |
pip install git+https://github.com/Coding-Dev-Tools/apighost.git
pip install pytest requests
- name: Run tests (mock server starts inside pytest fixture)
run: pytest tests/
No extra services to spin up in GitHub Actions. The mock_api fixture handles the lifecycle.
| Situation | Recommendation |
|---|---|
| Smoke tests / schema coverage | Live faker (apighost serve) — always reflects spec shape |
| Assertions on specific values | VCR cassette (apighost replay) — deterministic |
| Error path testing | --scenario error against live faker or cassette |
| CI on every PR | VCR cassette committed to repo — no network dependency |
The Free tier covers local and dev use — the fixture above runs entirely free. Pro ($12/mo or $119/yr) includes unlimited requests, VCR cassettes, and CI/CD integration. Suite ($49/mo) bundles all Coding-Dev-Tools products under one license.
responses, httpretty, or pytest-httpserver?Those tools are fine for unit tests where you control every response. APIGhost is the better fit when you have an OpenAPI spec and want generated coverage without maintaining stubs. When the spec changes, regenerate the cassette — no JSON hunting.