How to Test and Mock the Mistral AI API
Every test that calls the live Mistral AI API is a test that can fail because of someone else's deploy. Here is where to draw the line between mocked and real โ and what each side is actually allowed to tell you.
๐ก Monitor your APIs โ know when they go down before your users do
Better Stack checks uptime every 30 seconds with instant Slack, email & SMS alerts. Free tier available.
Affiliate link โ we may earn a commission at no extra cost to you
The first integration test anyone writes against Mistral AI calls the real endpoint, asserts the model said something sensible, and passes. It keeps passing for about three weeks. Then it fails on a Sunday because of a rate limit, fails again on a fork's pull request because the key is not there, fails a third time because the model phrased its answer differently โ and someone adds `retry: 3` and stops reading the output.
The short version
Mock the network, not the SDK. Record fixtures from one real call rather than hand-writing them. Spend most of your testing effort on the failure responses, because those are the paths that only ever run during an incident. And keep one live check outside CI โ a suite full of mocks cannot tell you Mistral AI is down.
Why CI Should Not Call Mistral AI Directly
None of these are theoretical. Each one is a specific way a live-calling test suite degrades into a suite nobody trusts.
- Your build inherits their uptime. A Mistral AI incident becomes a red build across every branch, and the team learns that red means "probably upstream" โ which is exactly the habit that lets a real regression through.
- Every pull request costs money. Token spend scales with contributor activity rather than with anything you would choose to pay for, and it is invisible until the invoice.
- Secrets have to exist everywhere CI runs. That includes fork pull requests, where a real
MISTRAL_API_KEYeither fails to exist (so external contributors can never get a green build) or does exist (which is a far worse problem). - Model output is not deterministic. Asserting on generated prose produces failures that are not bugs, and the standard fix โ loosening the assertion until it always passes โ leaves a test that costs money and proves nothing.
- Rate limits look exactly like flakiness. A parallelised suite trips Mistral AI's limits precisely because it is fast, and the 429 arrives as an intermittent failure rather than as a clear message about concurrency.
The Four Layers, and What Each One Proves
Most teams have layer one and layer four missing, then wonder why a green suite did not prevent the outage. Each layer answers a different question, and no layer answers another layer's.
| Layer | Runs | Proves | Cannot tell you |
|---|---|---|---|
| Unit / mocked | Every push | Your prompt construction, routing, parsing, retry and error-handling logic | Nothing about the provider itself |
| Recorded fixtures | Every push | That your code survives real response bodies, including error bodies | Whether those bodies are still current |
| Live smoke test | Pre-deploy, or nightly | Credentials work, model ids resolve, the contract still holds | Behaviour under real production load |
| Synthetic monitoring | Continuously | That the provider is up and fast right now, from outside your app | Nothing โ this is the layer that pages you |
The line worth internalising: layers one and two are about your code, layer three is about your assumptions, and layer four is about Mistral AI. Adding more of the first two will never substitute for the last one.
A Mock You Can Paste In
Mistral AI is close to OpenAI-shaped, but served by its own SDK, and most codebases reach it through the `@mistralai/mistralai` client. Intercept at the HTTP boundary so the real client still runs โ its validation, its error classes, its stream parsing all stay in the test.
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.post('https://api.mistral.ai/v1/chat/completions', () =>
HttpResponse.json(SUCCESS_FIXTURE)),
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// onUnhandledRequest: 'error' is the important line. Without it, a call your
// code makes to an endpoint you forgot to stub goes to the real api.mistral.ai
// with whatever key is in the environment.Get the success fixture by making one real call and saving the response verbatim, not by writing out what you think the body looks like. A hand-authored fixture tests your code against your own assumptions, which it will always pass.
Simulate the Four Failures That Actually Happen
This is where mocking earns its cost. Every one of these paths runs for the first time during an incident unless a test made it run earlier.
// 1. Rate limited โ assert the backoff reads retry-after.
http.post('https://api.mistral.ai/v1/chat/completions', () =>
new HttpResponse(null, { status: 429, headers: { 'retry-after': '2' } }));
// 2. Server error โ assert a bounded retry, then a clean surfaced failure.
http.post('https://api.mistral.ai/v1/chat/completions', () =>
new HttpResponse(null, { status: 500 }));
// 3. Timeout โ assert your own deadline fires before the user's patience does.
http.post('https://api.mistral.ai/v1/chat/completions', async () => {
await new Promise((r) => setTimeout(r, 30_000));
return HttpResponse.json(SUCCESS_FIXTURE);
});
// 4. Truncated stream โ the one nobody tests, and the one that
// quietly writes half an answer into your database as if it were whole.
http.post('https://api.mistral.ai/v1/chat/completions', () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(
'data: {"choices":[{"delta":{"content":"The ans"}}]}\n\n'));
controller.close(); // no [DONE], no finish_reason
},
});
return new HttpResponse(stream, {
headers: { 'Content-Type': 'text/event-stream' },
});
});The truncated stream is the expensive one
A stream that ends without a terminator is not an error at the HTTP layer โ the status was 200 and bytes arrived. Code that accumulates deltas and writes the result on stream-close will happily store half an answer and mark it complete. Assert that a stream ending without a finish signal is treated as a failure, not as a short response.
The Mistral Trap: Mocking the SDK Instead of the Wire
Mistral ships its own client rather than asking you to repoint the OpenAI SDK, and the path of least resistance in a test file is to `jest.mock('@mistralai/mistralai')` and hand back an object shaped like whatever your code happens to read. That mock passes forever. It also stops testing anything the moment Mistral changes a field name, adds a required parameter, or alters how the client raises an error โ because the thing you replaced is the thing that would have changed.
Mock at the HTTP boundary instead. Intercept the request to api.mistral.ai and return a real response body, so your code still runs through the actual client: its validation, its error classes, its streaming parser. When the SDK's behaviour changes under you, a wire-level mock notices at upgrade time; an SDK-level mock notices in production.
The `-latest` model aliases are the same category of hazard in a different coat. Pinning `mistral-large-latest` in a fixture records the behaviour of whichever snapshot was live the day you recorded it, and the alias silently moves. If a test asserts anything about the *content* of a completion, pin a dated model id in the test path so the assertion has something stable to be true about.
Intercept the wire, keep the real client
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// The real Mistral client still runs โ only the network is faked.
export const server = setupServer(
http.post('https://api.mistral.ai/v1/chat/completions', async ({ request }) => {
const body = await request.json();
// Assert on what your code actually sent.
if (!body.model || !Array.isArray(body.messages)) {
return HttpResponse.json({ message: 'invalid request' }, { status: 422 });
}
return HttpResponse.json({
id: 'cmpl-test',
object: 'chat.completion',
model: body.model,
choices: [{
index: 0,
message: { role: 'assistant', content: 'ok' },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 9, completion_tokens: 1, total_tokens: 10 },
});
}),
);The One Test That Should Hit the Real API
Keep exactly one, keep it out of the pull-request path, and give it a narrow job: prove that the credentials work, that the model id still resolves, and that the response still has the shape your fixtures claim. It is the only thing standing between a fully mocked suite and a fixture that has quietly stopped describing reality.
// One test. Runs pre-deploy or nightly, never on a pull request.
// Its job is to prove the assumptions the mocks are built on.
const res = await fetch('https://api.mistral.ai/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MISTRAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'mistral-large-latest',
messages: [{ role: 'user', content: 'Reply with the single word: ok' }],
max_tokens: 5,
}),
});
if (!res.ok) {
throw new Error(`Mistral AI smoke test failed: ${res.status}`);
}
// Assert the SHAPE the fixtures encode, not the words that came back.
const body = await res.json();
assertMatchesFixtureShape(body);Run it on a schedule and before deploy. Do not run it on pull requests, because that reintroduces every problem in the first section โ the secret exposure, the fork failures, the upstream-outage red builds โ in exchange for a signal you already get nightly.
Where Testing Stops and Monitoring Starts
A test suite runs when you push. An outage happens when it happens. That gap is the entire reason a green pipeline and a broken product coexist so comfortably: nothing in CI is watching Mistral AI at 02:00 on a Saturday, and a nightly smoke test tells you about a failure up to twenty-four hours after your users found it.
The layer that closes the gap is independent probing โ real requests to api.mistral.ai on an interval, from outside your infrastructure, alerting on both errors and latency. It is also what answers the question your team will actually ask during an incident, which is never "did the tests pass" but always "is it us or is it them?"
Frequently Asked Questions
Should my CI pipeline call the live Mistral AI API?
Almost never on every push. Live calls in CI make your build depend on someone else's uptime, cost money on every pull request, leak a real MISTRAL_API_KEY into every fork and pull-request runner, and produce non-deterministic failures that teams eventually silence by retrying. Keep the pull-request path fully mocked, and run a small live smoke test on a schedule or before deploy where a failure means something and a secret can be handled properly.
What is the difference between mocking the Mistral AI SDK and mocking the HTTP layer?
Mocking the SDK replaces the client object, so your test never exercises the client's own validation, error classes or stream parsing โ the parts most likely to change under you. Mocking the HTTP layer (msw, nock, a local server, or a base-URL override) intercepts the request to api.mistral.ai and lets the real client run against a fake network. The second is more work to set up once and catches an entire category of bug the first cannot see, including SDK upgrades that change behaviour silently.
How do I test what my code does when Mistral AI returns a 429 or a 500?
Make the mock return them. This is the single highest-value thing a mock does, because the failure paths are the ones that are never exercised by hand and are always the ones that break in an incident. Return a 429 with a retry-after header and assert the backoff honours it rather than sleeping a hardcoded amount; return a 500 and assert the request is retried a bounded number of times; return a stream that ends mid-message and assert the partial output is not silently persisted as complete.
Should I use a dated Mistral model id in tests instead of mistral-large-latest?
In any test that asserts on the text of a completion, yes. A `-latest` alias is a moving pointer, and a moving pointer under a golden-output assertion produces a failure that looks like your bug and is actually a model release. Pin a dated snapshot id in the test path, keep the alias in production if you want automatic upgrades, and treat the gap between them as something you deliberately review rather than something that surprises you on a Tuesday morning.
If everything is mocked, how do I know when Mistral AI is actually down?
You do not, and that is the honest limit of a test suite: mocks tell you your code is correct, not that the provider is available. Availability is a monitoring question, not a testing one. Something outside your application has to be making real requests to api.mistral.ai on an interval and alerting when they fail or slow down โ otherwise the first signal of an outage is a user, and the first thing your team will do is spend twenty minutes deciding whether the bug is yours.
Related Guides
Your Tests Cannot Page You at 2am
Mocks prove your code is right. They cannot tell you Mistral AI stopped answering an hour ago. API Status Check probes Mistral AI and the rest of your stack independently and alerts on errors and latency, so "is it us or is it them?" has an answer before anyone opens a terminal.
Start Your Free Trial โ๐ Tools We Use & Recommend
Tested across our own infrastructure monitoring 200+ APIs daily
Uptime Monitoring & Incident Management
Used by 100,000+ websites
Monitors your APIs every 30 seconds. Instant alerts via Slack, email, SMS, and phone calls when something goes down.
โWe use Better Stack to monitor every API on this site. It caught 23 outages last month before users reported them.โ
Secrets Management & Developer Security
Trusted by 150,000+ businesses
Manage API keys, database passwords, and service tokens with CLI integration and automatic rotation.
โAfter covering dozens of outages caused by leaked credentials, we recommend every team use a secrets manager.โ
SEO & Site Performance Monitoring
Used by 10M+ marketers
Track your site health, uptime, search rankings, and competitor movements from one dashboard.
โWe use SEMrush to track how our API status pages rank and catch site health issues early.โ