SDKs & Developer Experience

pytest Patterns for Claude SDK Integration Tests

Claude calls are slow, non-deterministic, and metered by the token. A few pytest habits keep your test suite fast, cheap, and honest about which tests actually hit the network.

Claude 3P 101 · Updated July 2026 · Unofficial guide

Testing code that calls Claude is different from testing code that calls your own database. Every real request costs money, takes seconds rather than milliseconds, and can return slightly different text each time. The standard answer is a two-layer suite: fast unit tests that never touch the network (using mocks or recorded responses), plus a small set of true integration tests that call the live API and are run deliberately — in a nightly job or a pre-release gate, not on every save. pytest, the de facto Python test runner, gives you clean tools for both layers.

Fixtures: build the client once, skip cleanly without credentials

A pytest fixture is a reusable piece of setup that tests request by naming it as an argument. The Python SDK (installed with pip install anthropic, Python 3.9 or newer) reads the ANTHROPIC_API_KEY environment variable automatically when you construct Anthropic() with no arguments, so the fixture's main job is deciding what to do when that variable is absent — for example on a laptop with no key configured. Skipping is better than failing: it tells the truth ("this test didn't run") without breaking the build for everyone.

import os, pytest
from anthropic import Anthropic

@pytest.fixture(scope="session")
def client():
    if not os.environ.get("ANTHROPIC_API_KEY"):
        pytest.skip("integration tests need ANTHROPIC_API_KEY")
    return Anthropic()

@pytest.mark.integration
@pytest.mark.parametrize("model", ["claude-haiku-4-5", "claude-sonnet-5"])
def test_reply_is_nonempty(client, model):
    resp = client.messages.create(model=model, max_tokens=64,
        messages=[{"role": "user", "content": "Say hello in one word."}])
    assert resp.content[0].text.strip()

scope="session" means the client is constructed once per test run rather than once per test. The @pytest.mark.integration marker lets you split the layers from the command line: pytest -m "not integration" for the fast local loop, pytest -m integration in the scheduled job. Register the marker in your pytest configuration so typos fail loudly.

Watch the empty-string trap: per Anthropic's credential documentation, an environment variable set to an empty string still occupies its slot in the SDK's credential-resolution order — ANTHROPIC_API_KEY="" selects the API-key path with an empty key instead of falling through. In CI, unset unused credential variables rather than blanking them, and have your fixture treat empty as missing (as above).

Parametrize: one test, many models

@pytest.mark.parametrize runs the same test body once per parameter — in the example, once against Claude Haiku 4.5 and once against Claude Sonnet 5. This is the cheapest way to catch "works on the model we developed against, breaks on the model we deploy" surprises. Keep the default parameter list short and cheap (Haiku 4.5 is the low-cost option), and expand it only in the pre-release run. The same trick works for parametrizing prompts, expected properties, or platform clients if you run on more than one of them.

Assert on properties, not exact strings

Model output varies between runs, so exact-match assertions rot quickly. Assert on properties instead: the response is non-empty, it parses as JSON, it contains a required field, it stays under a length limit, the SDK returned the expected stop reason. If a business rule genuinely requires judging text quality, that belongs in an evaluation harness, not a pass/fail unit test.

Async code and asyncio mode

If your application uses the SDK's asynchronous client, your tests are async def functions, and plain pytest will not await them. The common solution is the pytest-asyncio plugin: install it and set asyncio_mode = "auto" in your pytest configuration so async tests are collected and awaited without per-test decorators. The fixture pattern above carries over — construct the async client in a fixture and share it across tests. For the async client's exact class name and usage, check the official SDK repository at github.com/anthropics/anthropic-sdk-python rather than guessing from the synchronous API.

Keep the bill boring

Three habits keep integration-test spend negligible: use the cheapest model that exercises the code path (Haiku 4.5 lists at $1 per million input tokens); cap max_tokens low, since you are testing plumbing rather than prose; and keep the integration suite small — a handful of end-to-end tests plus a broad mocked suite beats hundreds of live calls. For the mocked layer, see mocking the Claude SDK and record-and-replay with VCR cassettes.

Where to go next

Wire the suite into CI with the Claude SDK in GitHub Actions and CI/CD secrets injection, or start from the quickstart if you haven't made a first call yet.

Sources