Mock the SellerMagnet API in Python tests by faking only requests.Session.get, never your own client module: patch the one call the real sellermagnet.py and retry.py helpers make, script the status code and body each test needs, and run the unchanged client code against it. No credit is spent, no network call leaves the test, and the exact parsing and retry logic under test is the code that ships.
Key Takeaways
- Patch
requests.Session.getonly; a hand-written mock of your own client module can drift from the real one and still pass. - Make the fake fail closed: a request before a test installs its scripted response should raise, not silently reach the real API.
- A 429 is free and worth waiting out; a 500 is refunded and worth one retry; a 502/503 is billed on every attempt, so cap the attempts.
- Patch
time.sleepin the test so a retry's backoff runs instantly, then assert the recorded wait still honouredRetry-After. - Count every call the fake received to verify the credit math of a scenario, not just its final return value.
Why not just call the real API in a test suite?
Every SellerMagnet API request is billed per the table below, whether it comes from production code or a test. A CI run that fires on every push would spend credits on every build, a slow or flaky network turns a code bug into a failed pipeline, and a 404 or a 429 that a test wants to simulate on demand cannot be forced on the live API at all. A fake HTTP layer makes every status code - including ones that are rare in practice - reproducible on every run.
| Scenario | Charged | Retry? | Client should |
|---|---|---|---|
| happy path | billed | no | parse and return |
| 404 product not found | billed | no | report it; do not retry |
| free gate 400 (missing marketplaceId) | free | no | fix the request, no retry |
| 401 insufficient credits | free | no | stop; top up credits |
| 429 concurrency cap | free | yes | sleep Retry-After, then retry |
| 500 internal error | refunded | yes | retry once, then stop |
| 502 / 503 upstream | billed | yes | retry up to the attempt cap |
How do I fake just the HTTP layer, not my own code?
Replace requests.Session.get with a function that returns a scripted status, JSON body and headers, then let sellermagnet.py's get() and retry.py's get_with_retry() (both from the Python quickstart) run exactly as they would in production. The fake defaults to raising, so a test that forgets to script a response fails loudly instead of letting a real, billed request through.

mock_api.py - fakes only requests.Session.get, fails closed
import os
import requests
os.environ.setdefault("SELLERMAGNET_API_KEY", "test-key")
def _blocked(*a, **k):
raise RuntimeError("mock_api: call install(route) first - no request may reach the real API")
requests.Session.get = _blocked
CALLS = [] # every (endpoint, params) the code under test sent
class _Response:
def __init__(self, status, body, headers=None):
self.status_code, self._body, self.headers = status, body, headers or {}
def json(self):
if isinstance(self._body, str): # simulate an HTML error page
raise ValueError("not json")
return self._body
def ok(data):
"""200 with the envelope the API wraps every successful response in."""
return 200, {"success": True, "data": data}
def error(status, message="request failed", headers=None):
"""A non-2xx or a 200/success:false envelope, with optional headers (Retry-After)."""
return status, {"success": False, "error": "error", "message": message}, headers or {}
def install(route):
"""route(endpoint, params) -> (status, body) or (status, body, headers).
Replaces requests.Session.get for the lifetime of the test; also asserts the two
things every request must do: send the key as a header, and set a timeout.
"""
def fake_get(self, url, params=None, timeout=None):
assert timeout == 120, "sellermagnet.get() must set timeout=120"
assert self.headers.get("X-Api-Key") == "test-key", "key must travel as a header"
endpoint = url.rsplit("/", 1)[1]
CALLS.append((endpoint, dict(params or {})))
result = route(endpoint, dict(params or {}))
status, body, headers = (*result, {}) if len(result) == 2 else result
return _Response(status, body, headers)
requests.Session.get = fake_get

What does a full pytest scenario suite look like?
A pytest fixture installs one scripted route per test and resets the fake back to its fail-closed default afterwards, so tests cannot leak a scripted response into the next one. monkeypatch.setattr(time, "sleep", ...) keeps a 429 or 500 retry test from actually waiting, while still letting the test assert on the duration the code under test tried to sleep for.
conftest.py - the fake_api fixture every test below uses
import mock_api
import pytest
@pytest.fixture
def fake_api():
"""Installs the test's scripted route, then resets to the fail-closed default."""
yield mock_api.install
mock_api.CALLS.clear()
mock_api.requests.Session.get = mock_api._blocked
test_sellermagnet.py - the scenario suite, run for real below
import time
import pytest
import mock_api
from sellermagnet import ApiError, get, lookup, new_session
from retry import get_with_retry
ASIN, MP = "B0CL61F39H", "ATVPDKIKX0DER"
def _product(price=444.99):
return {"productInfo": {
"asin": ASIN, "title": "PlayStation5 console (slim)",
"buyBoxInfo": {"price": price, "currencyCode": "USD"},
"bestsellerRanks": {"main_category": {"name": "Video Games", "rank": 31}},
"link": f"https://www.amazon.com/dp/{ASIN}",
}}
def test_happy_path_parses_the_real_response_shape(fake_api):
fake_api(lambda ep, p: mock_api.ok(_product()))
out = lookup(new_session(), ASIN, MP)
assert out == {"asin": ASIN, "title": "PlayStation5 console (slim)", "price": 444.99,
"currency": "USD", "rank": 31, "url": f"https://www.amazon.com/dp/{ASIN}"}
assert mock_api.CALLS == [("amazon-product-lookup", {"asin": ASIN, "marketplaceId": MP})]
def test_no_featured_offer_price_is_not_a_number(fake_api):
"""buyBoxInfo.price can be the string "N/A"; must come back as None, never 0 or a str."""
fake_api(lambda ep, p: mock_api.ok(_product(price="N/A")))
out = lookup(new_session(), ASIN, MP)
assert out["price"] is None
def test_404_is_billed_and_must_not_be_retried(fake_api):
"""A 404 is a real answer (ASIN not on that marketplace); retrying only pays for it again."""
fake_api(lambda ep, p: mock_api.error(404, "Product not found."))
with pytest.raises(ApiError) as exc:
get_with_retry(new_session(), "amazon-product-lookup", asin=ASIN, marketplaceId=MP)
assert exc.value.status == 404
assert len(mock_api.CALLS) == 1, "404 must not trigger a retry"
def test_free_gate_400_missing_marketplace_id(fake_api):
"""Raised by request validation before billing; distinct from an endpoint's own 400."""
fake_api(lambda ep, p: mock_api.error(400, "Missing required parameter(s): marketplaceId"))
with pytest.raises(ApiError) as exc:
get(new_session(), "amazon-product-lookup", asin=ASIN)
assert exc.value.status == 400
assert "marketplaceId" in str(exc.value)
def test_401_insufficient_credits_is_not_retried(fake_api):
fake_api(lambda ep, p: mock_api.error(401, "Insufficient user credits."))
with pytest.raises(ApiError) as exc:
get_with_retry(new_session(), "amazon-product-lookup", asin=ASIN, marketplaceId=MP)
assert exc.value.status == 401
assert len(mock_api.CALLS) == 1
def test_429_concurrency_cap_is_free_and_retried(fake_api, monkeypatch):
"""429 carries Retry-After; get_with_retry must wait at least that long, then succeed."""
monkeypatch.setattr(time, "sleep", lambda s: slept.append(s))
slept = []
calls = {"n": 0}
def route(ep, p):
calls["n"] += 1
if calls["n"] == 1:
return mock_api.error(429, "Max 40 concurrent requests per API key. Retry shortly.",
{"Retry-After": "2"})
return mock_api.ok(_product())
fake_api(route)
out = get_with_retry(new_session(), "amazon-product-lookup", asin=ASIN, marketplaceId=MP)
assert out["productInfo"]["asin"] == ASIN
assert len(mock_api.CALLS) == 2
assert slept and slept[0] >= 2.0, "must honour Retry-After, not a shorter backoff"
def test_500_is_refunded_and_retried_exactly_once(fake_api, monkeypatch):
"""A second consecutive 500 must raise, not loop forever (retry.py's internal_errors > 1 check)."""
monkeypatch.setattr(time, "sleep", lambda s: None)
fake_api(lambda ep, p: mock_api.error(500, "Internal error."))
with pytest.raises(ApiError) as exc:
get_with_retry(new_session(), "amazon-product-lookup", asin=ASIN, marketplaceId=MP)
assert exc.value.status == 500
assert len(mock_api.CALLS) == 2, "exactly one retry after a 500, then stop"
def test_502_is_billed_and_retried_up_to_the_attempt_cap(fake_api, monkeypatch):
monkeypatch.setattr(time, "sleep", lambda s: None)
fake_api(lambda ep, p: mock_api.error(502, "Upstream error."))
with pytest.raises(ApiError) as exc:
get_with_retry(new_session(), "amazon-product-lookup", asin=ASIN, marketplaceId=MP, attempts=3)
assert exc.value.status == 502
assert len(mock_api.CALLS) == 3, "502 is billed on every attempt, so the cap must be respected"
def test_mock_fails_closed_without_install():
"""If a test forgets fake_api, the code must never reach the real API."""
import requests
with pytest.raises(RuntimeError, match="install"):
requests.Session().get("https://sellermagnet-api.com/api/amazon-product-lookup")
pytest -v output (unmodified run)
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
collected 9 items
test_sellermagnet.py::test_happy_path_parses_the_real_response_shape PASSED [ 11%]
test_sellermagnet.py::test_no_featured_offer_price_is_not_a_number PASSED [ 22%]
test_sellermagnet.py::test_404_is_billed_and_must_not_be_retried PASSED [ 33%]
test_sellermagnet.py::test_free_gate_400_missing_marketplace_id PASSED [ 44%]
test_sellermagnet.py::test_401_insufficient_credits_is_not_retried PASSED [ 55%]
test_sellermagnet.py::test_429_concurrency_cap_is_free_and_retried PASSED [ 66%]
test_sellermagnet.py::test_500_is_refunded_and_retried_exactly_once PASSED [ 77%]
test_sellermagnet.py::test_502_is_billed_and_retried_up_to_the_attempt_cap PASSED [ 88%]
test_sellermagnet.py::test_mock_fails_closed_without_install PASSED [100%]
============================== 9 passed in 0.03s ===============================
Never hand-write a mock of sellermagnet.py itself
A mock that reimplements lookup() or get_with_retry() tests the mock's own logic, not the real client. Fake the HTTP call one level below your code, so a real bug in the parsing or retry logic still shows up as a failing test.
Does the same pattern work for async or other languages?
Yes - fake the one call the real code makes to the network, whatever that call is. The asyncio + httpx guide makes concurrent lookups through httpx.AsyncClient; a test for it fakes AsyncClient.get the same way this guide fakes Session.get. The 429/Retry-After guide and the 502/503 retry guide are the production behaviour this test suite is checking against; keep all three in sync when the retry logic changes. Credit costs and the free trial are on the pricing page, and every endpoint's parameters are in the API documentation and code examples.
Frequently Asked Questions
Can my tests call the real SellerMagnet API instead of mocking it?
They can, but every call is billed like any other request, a CI run that fires on every push burns credits fast, and a flaky network turns a code bug into a failed build. Faking only the HTTP layer keeps the real client code under test while the API itself never sees a request.
What exactly should I fake: my own client module, or the HTTP library?
The HTTP library call (requests.Session.get), never your own sellermagnet.py or retry.py. A hand-written mock of your own module can drift from the real one and still pass; faking only .get() means the exact parsing, retry and error code under test is the code that ships.
How do I simulate a 429 concurrency cap without waiting 20 seconds in every test run?
Patch time.sleep in the test (pytest's monkeypatch.setattr) so the retry backoff in retry.py runs instantly, then assert the recorded sleep duration was still at least the Retry-After value the fake response sent, instead of actually waiting for it.
Does a fake HTTP layer also test the credit math?
Yes, if the fake records every call it receives. Counting the recorded calls after a 429 or a refunded 500 verifies the exact number of billed attempts a scenario produces, which is the same number a real burst would be charged.
Should the fake HTTP layer allow a real request if a test forgets to configure it?
No. Make the default requests.Session.get raise immediately, and have each test install its own fake response before running the code under test. That way a forgotten setup fails the test loudly instead of quietly sending a real, billed request.
Bottom line: fake requests.Session.get (or your language's equivalent), run the real client code against scripted responses for every billed, free and refunded status, and make the fake fail closed so a forgotten setup cannot send a real, billed request. The whole suite above runs in under a tenth of a second and spends zero credits.