Building an AI agent is fun. At least, I have had so much fun building out Ozigi, a social media content manager agent (ps, we are in need of user experience testers!).
But!
Testing it in a CI/CD pipeline is a nightmare.
If you are building an application that relies on an LLM (like OpenAI, Anthropic, or Google's Vertex AI), you quickly run into these three challanges when writing End-to-End (E2E) tests:
- Cost: Every time your test suite runs, you are burning API credits.
- Speed: LLMs are slow. Waiting 10-15 seconds per test will grind your deployment pipeline to a halt.
-
Non-Determinism: LLMs never return the exact same string twice. If your Playwright test relies on
expect(page.getByText('exact phrase')).toBeVisible(), your tests will randomly fail.
While building Ozigi—an agentic content engine designed to turn raw technical research into structured social campaigns—I needed a way to test the complex UI state transitions (like custom loaders and dynamic grids) without actually hitting the Vertex AI API, especially seeing as I am managing very conservatively my $300 in credits!
Playwright Network Interception
Here is how to completely decouple your frontend E2E tests from your LLM backend using Next.js and Playwright.
In Ozigi, the user flow looks like this:
- The user selects a custom persona and inputs raw context (a URL or text dump).
- They click "Generate Campaign."
- The UI swaps to a
<DynamicLoader />.
- The Next.js API route (
/api/generate) sends the context to Gemini 2.5 Pro. - The LLM returns a strictly formatted JSON object.
- The UI renders the multi-platform campaign grid.
If I test this live, it will introduce latency and flakiness.
Instead, I intercepted the API call and instantly return a fake JSON payload.
Network Mocking (Interception with page.route)
Playwright allows us to hijack outbound network requests directly from the browser. When the frontend tries to call our Next.js API route, Playwright intercepts the POST request, blocks it from ever hitting the server, and fulfills it with our own static data.
Here is the exact test script I use to validate the Ozigi content engine:
import { test, expect } from '@playwright/test';
test.describe('Ozigi Context Engine & AI Mocking', () => {
test('should generate a campaign by intercepting the LLM response', async ({ page }) => {
// 1. Navigate to the dashboard
await page.goto('/dashboard');
// 2. Fill out the Context fields
await page.getByPlaceholder('Paste a URL or raw notes').fill('https://ozigi.app/docs');
await page.getByPlaceholder('Additional directives...').fill('Keep it technical.');
// 🚀 THE MAGIC: Intercept the AI generation API route
await page.route('**/api/generate', async (route) => {
// Define the exact JSON structure your frontend expects from the LLM
const mockedAIResponse = {
output: JSON.stringify({
campaign: [
{
day: 1,
x: "Day 1 Thread: Ozigi is tested and working! 1/2\n\n[The content engine is officially alive.]",
linkedin: "LinkedIn Post: Ozigi testing complete.",
discord: "Discord Update: Systems green."
}
]
})
};
// Fulfill the route instantly with the mocked data
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockedAIResponse),
});
});
// 3. Trigger the generation
await page.getByRole('button', { name: /Generate Campaign/i }).click();
// 4. Assert the UI state transitions correctly
// Verify the loader appears while the "network" request is happening
const loaderContainer = page.locator('.animate-in.fade-in');
await expect(loaderContainer).toBeVisible();
// 5. Assert the final UI renders our mocked data perfectly
await expect(page.getByText('Ozigi is tested and working!')).toBeVisible();
await expect(page.getByText('[The content engine is officially alive.]')).toBeVisible();
});
});
Why You Should Mock LLM/API Responses In Playwright
By using this testing pattern, I achieved three of my engineering goals:
Zero Cost: The test suite can run 1,000 times a day on GitHub Actions without costing a single cent in Vertex AI compute.
Lightning Fast: The entire E2E test finishes in seconds, as I bypass the LLM's generation latency entirely.
Absolute Determinism: Because I injected a static JSON payload, my text assertions (
toBeVisible) will never fail due to an AI hallucination or a slightly altered adjective.
When building AI wrappers or agentic workflows, your testing strategy must isolate the LLM from the UI. Let the LLM be unpredictable in production, but demand strict predictability in your test suite.
I built this network mocking (interception0 pattern into Ozigi, an agentic content engine that helps pretty much anyone turn their raw notes/ideas into structured, multi-platform campaigns without dealing with cheesy AI buzzwords. You can check it out at ozigi.app.
Let's connect on LinkedIn!
You can find my spaghetti code here..
Consider this the unofficial v3 changelog of Ozigi. As always, we are welcome to your feedback and can't wait to hear from you!



Top comments (1)
Insightful read! I like how these are drawn from your own personal experience building it. Thanks for sharing!