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.
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.