How to Test and Mock the Perplexity API
Every test that calls the live Perplexity 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 Perplexity 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 Perplexity is down.
Why CI Should Not Call Perplexity 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 Perplexity 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
PERPLEXITY_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 Perplexity'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 Perplexity. Adding more of the first two will never substitute for the last one.
A Mock You Can Paste In
Perplexity is OpenAI-compatible, plus search fields, and most codebases reach it through the OpenAI SDK pointed at Perplexity, or plain fetch. 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.perplexity.ai/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.perplexity.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.perplexity.ai/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.perplexity.ai/chat/completions', () =>
new HttpResponse(null, { status: 500 }));
// 3. Timeout — assert your own deadline fires before the user's patience does.
http.post('https://api.perplexity.ai/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.perplexity.ai/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 Perplexity Trap: The Answer Is Supposed to Change
Every other provider on this list is non-deterministic in the way a language model is non-deterministic — set the temperature low, pin the model, and yesterday's output is roughly today's output. Perplexity is different in kind: it grounds answers in a live web search, so the correct answer to the same question genuinely changes as the web changes. A golden-output test against a Perplexity completion is not flaky, it is wrong.
That reframes what the assertion should be. Do not assert the prose. Assert the *shape and the contract*: that citations came back, that they are absolute URLs, that your parser survives a response with zero citations, that a citation list longer than your UI expects does not break the render. Those properties are stable even though the content is not.
The recency dimension deserves its own fixture set. Search-grounded responses degrade in a specific way — a request scoped to recent results can legitimately return nothing, and code written against a happy-path fixture usually crashes on the empty case rather than degrading. Record an empty-citation response deliberately and keep it, because you will not get one on demand from the live API.
Assert the contract, not the prose
const data = await res.json();
const answer = data.choices[0].message.content;
const citations = data.citations ?? [];
// ❌ Will fail the week the web changes.
// expect(answer).toBe('The capital of France is Paris.');
// ✅ Stable properties of a search-grounded response.
expect(typeof answer).toBe('string');
expect(answer.length).toBeGreaterThan(0);
expect(Array.isArray(citations)).toBe(true);
for (const url of citations) {
expect(() => new URL(url)).not.toThrow();
}
// And the case the happy path never gives you:
// a recorded fixture with citations: [] must render, not crash.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.perplexity.ai/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PERPLEXITY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'sonar',
messages: [{ role: 'user', content: 'Reply with the single word: ok' }],
max_tokens: 5,
}),
});
if (!res.ok) {
throw new Error(`Perplexity 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 Perplexity 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.perplexity.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 Perplexity 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 PERPLEXITY_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 Perplexity 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.perplexity.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 Perplexity 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.
Can I write a golden-output test against the Perplexity API at all?
Not against the answer text, and trying is the most common reason Perplexity test suites get muted. The response is grounded in live search results, so it is meant to change when the underlying sources change — a test that pins the prose will fail for reasons that have nothing to do with your code, and the team will eventually delete it. Golden-test the things that do not move: the response schema, citation URL validity, empty-citation handling, and your own prompt-construction logic, which you can assert on exactly because it never leaves your process.
If everything is mocked, how do I know when Perplexity 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.perplexity.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 Perplexity stopped answering an hour ago. API Status Check probes Perplexity 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.”