Testing code that calls an LLM
Unit tests around a non-deterministic dependency — stubbing, recording and asserting on the parts that are yours rather than the model's.
A test suite that calls a real model is not a test suite. It is a monitoring job with a slow feedback loop and a bill, and it fails for reasons that have nothing to do with the change under review. The instinct that follows is usually to assert on the model’s words anyway and mark the test flaky — which throws away the one thing a test is for, the ability to say that a red run means somebody broke something.
The way out is a boundary. Almost none of the code you are worried about is the model: it is the prompt you assembled, the retry you wrote, the timeout you set, the parser that turns a completion into a domain object, and the tool the model asked you to run. All of that is deterministic, and all of it can be tested at full speed with no network if the seam is in the right place. These pages are about putting it there and then about the harder half — deciding what is worth asserting once the output on the other side is not fixed.
Testing Node.js Code That Calls an LLM
Where to cut a Node service so the model is out of the test, using msw at the HTTP boundary and assertions on the request you sent rather than the answer you got.
10 min read
Dependency Injection for Testing an LLM Client
Why a module-level client turns a five-line fake into a rewrite, and the smallest seam that makes an LLM call testable without a mocking framework.
9 min read
Testing a Repository Pattern Wrapped Around an LLM Call
How the repository pattern changes when the store behind it is a model — partial results, a price per read, and a return value you cannot compare for equality.
10 min read
Unit Testing a Prompt-Building Function
Prompt assembly is string code with no network in it, and the five assertions worth making on it — substitution, delimiting, prefix stability, budget and loud failure.
9 min read
Stubbing an LLM Client Without a Mocking Library
A hand-written fake that implements the same interface, records its calls and scripts its answers — and an honest account of the four things it does worse than a mocking framework.
9 min read
Testing Retry Logic Around an LLM API Call
Forcing a transient failure on the first attempt so the test proves the retry executed, and turning off the SDK's own retries so you are testing your code and not theirs.
10 min read
Testing Exponential Backoff on a 429 From an LLM Provider
Asserting the delay sequence itself with a fake clock, so a backoff test finishes in milliseconds and still proves the waits were the right length.
10 min read
Testing a Timeout Around a Slow LLM Call
Simulating a hung response without waiting for one, and asserting the part everyone forgets — that the abandoned request was actually aborted.
10 min read
Testing Idempotency of a Retried LLM Request
The retry whose first attempt actually succeeded — how to reproduce it in a test, and why the dangerous duplicate is usually the tool call rather than the completion.
10 min read
Unit Testing an LLM Output Parser
Building a fixture corpus of everything a model plausibly returns, and testing the parser against it — never the model's behaviour, only your handling of it.
10 min read
VCR Cassettes for LLM Tests in Python
A working VCR.py setup for a Python LLM client, and the match_on mistake that quietly sends every test run back to the live API.
10 min read
Recording and Replaying OpenAI API Calls With nock
How to intercept the OpenAI Node SDK with nock, why older nock versions silently miss it entirely, and how to match a body the SDK rewrote.
10 min read
Mocking the OpenAI API With MSW in a Browser Test
Using Mock Service Worker for a model call made from the browser, including which URL you should actually be intercepting and the worker startup race.
10 min read
Stubbing an LLM API With WebMock in Ruby
Stubbing a chat completions call with WebMock's stub_request, and why hash_including does not do what you expect against a nested messages array.
9 min read
Mocking an LLM API With gock in Go
Using gock against an OpenAI-compatible Go client, starting with the transport interception that provider SDKs quietly break.
9 min read
Stubbing an LLM API With WireMock in Java
Running WireMock as a local stand-in for a chat completions API, for the common case where the Java client gives you no transport to patch.
10 min read
Mocking an LLM API With responses in Python
Hand-writing mock LLM responses with the responses library, and the client-library check that decides whether it can work for you at all.
9 min read
When a Cassette Goes Stale: Refreshing Recorded LLM Fixtures
Why a recorded fixture keeps a suite green against an API shape the provider retired, and the two mechanisms that make the drift visible.
9 min read
Redacting API Keys From Recorded LLM Cassettes
What to do the moment you find a live provider key in a committed test fixture, in the order that limits the damage.
9 min read
Recording Streaming Responses as Test Fixtures
Capturing a server-sent event stream as a fixture, and replaying it in chunks that split where a real network splits.
10 min read
Other topics
- LLM fundamentals & architecture
- Tokens, tokenization & context windows
- Prompt engineering
- Reasoning models & test-time compute
- Multimodal AI: vision, audio, video
- RAG & retrieval
- Embeddings & vector search
- AI agents & tool use
- Structured output & function calling
- Fine-tuning & post-training
- Local inference errors, string by string
- Running local models day to day
- Snapshot and property testing for model output
- Regression suites for prompts
- Eval gates in CI
- Flaky tests against a model
- Determinism and the cost of testing
- Contract and streaming tests
- Testing tool calls and retrieval
- Inference, serving & latency
- Rolling out a prompt change
- Testing AI systems in practice
- Forecasting a time series
- Machine learning on tabular data
- Geospatial data and models
- Understanding audio that is not speech
- Understanding video
- Core computer vision tasks
- Machine learning on graphs
- Point clouds and 3D
- Evaluation, benchmarks & LLM-as-judge
- Sensor and IoT data
- Logs and event streams
- Models over biological sequences
- Machine learning on molecules
- Embedding and searching code
- Extracting invoices and purchase orders
- Receipts, statements and tax forms
- Insurance policies and contracts
- Deeds, court filings and patents
- Extracting from medical records
- Observability & LLMOps
- CVs, certificates and identity documents
- Shipping, customs and technical documents
- Meetings, email, chat and filled-in forms
- Building an extraction pipeline
- Business, property and inspection documents
- Contract clauses and insurance claims
- Regulated and compliance documents
- Consumer, travel and closing documents
- Mapping one chat API onto another
- SDK and framework migrations
- Hallucination & failure modes
- Re-embedding and model deprecation
- Cutting over between providers
- Parity gaps, shims and legacy endpoints
- Moving between model versions
- Migrating vector stores and caches
- Mapping capabilities and parameters
- Migrating pipelines and agents
- Contracts, runbooks and rollback
- Auditing a codebase before a cutover
- Compliance and fine-tune migration
- LLM cost engineering
- Routing, cost tracking and multi-tenancy
- What a migration does to your prompts
- AI security & prompt injection
- Privacy, compliance & data residency
- AI governance, policy & society
- Building reliable AI applications
- AI hardware, GPUs & compute
- Open-weight models & local inference
- AI for developers & coding agents
- AI in industry: vertical playbooks
- AGI, superintelligence, alignment & the long future
- Machine learning foundations
- NLP fundamentals & classical tasks
- Data engineering for AI
- Synthetic data & dataset curation
- AI product design & UX
- Search, ranking & recommendation
- Enterprise adoption & change management
- AI careers, skills & teams
- Reading AI research
- AI in science & discovery
- Robotics & embodied AI
- AI economics, markets & business models
- AI myths, hype & media literacy
- Context engineering
- Shipping AI features: patterns & anti-patterns
- Build it: end-to-end AI tutorials
- Python for AI: hands-on recipes
- TypeScript, React and the web
- Frameworks and SDKs
- Errors and troubleshooting
- AI facts, numbers and statistics
- The history of AI
- The maths behind AI
- Architectures beyond the transformer
- Reinforcement learning
- Diffusion and generative media
- Speech, audio and voice engineering
- Benchmarks, one at a time
- AI search visibility
- Infrastructure and operations
- Databases and storage for AI
- Knowledge graphs and structured knowledge
- Classical ML in production
- Regulation, jurisdiction by jurisdiction
- Prompt recipes and pattern library
- AI for people who do not write code
- Writing, media and creative work
- Edge and on-device AI
- Interpretability and model internals
- Field notes
- OpenAI model behaviour
- Claude model behaviour
- Gemini model behaviour
- Llama model behaviour
- Mistral model behaviour
- Qwen model behaviour
- DeepSeek model behaviour
- Cohere model behaviour
- Grok model behaviour
- Small model behaviour
- Hybrid model architectures
- Token cost by language and script
- Transliteration, romanization and script handling
- Locale-correct output
- Multilingual generation quality
- Multilingual pipelines
- The EU AI Act, article by article
- AI under the GDPR and EU data law
- US AI regulation, state and sector
- International AI governance and standards
- AI litigation and enforcement
- Running AI workloads on AWS
- Running AI workloads on Google Cloud
- Running AI workloads on Azure
- AI at the edge: Workers, Vercel and Netlify
- Serving models on Kubernetes
- Operating AI infrastructure
- Quantization formats and what they cost
- llama.cpp, flag by flag
- Ollama and the desktop local-model runtimes
- Local models on Apple Silicon
- Hardware for local inference
- Running speech and embedding models locally
- Model files, adapters and conversion
- VRAM arithmetic for local models