Home / Blog / Mock the SellerMagnet API in Python Tests Without Spending Credits

Mock the SellerMagnet API in Python Tests Without Spending Credits

Test the code that calls the SellerMagnet API without hitting it: fake only `requests.Session.get`, run the real `sellermagnet.py` / `retry.py` helpers from the Python quickstart unchanged, and cover every billed, free and refunded status code with a pytest fixture that fails closed.

November 09, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Terminal window showing a pytest run that fakes the SellerMagnet API's HTTP layer while the real client code runs unchanged

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.get only; 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.sleep in the test so a retry's backoff runs instantly, then assert the recorded wait still honoured Retry-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.

Scenarios worth testing, and what the fake needs to script for each.
ScenarioChargedRetry?Client should
happy pathbillednoparse and return
404 product not foundbillednoreport it; do not retry
free gate 400 (missing marketplaceId)freenofix the request, no retry
401 insufficient creditsfreenostop; top up credits
429 concurrency capfreeyessleep Retry-After, then retry
500 internal errorrefundedyesretry once, then stop
502 / 503 upstreambilledyesretry 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.

Blueprint sequence diagram of a pytest fixture faking the SellerMagnet API's HTTP layer while the real client code runs unchanged
Only the HTTP call is faked; lookup() and get_with_retry() run unmodified.

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
Blueprint table of test scenarios against the SellerMagnet API's status codes, billing and the correct client reaction
Each row needs its own scripted response; a happy-path test alone misses most of this.

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.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime