Network Mocking and API Testing in Playwright: Complete Guide
End-to-end tests usually hit real backend services, but what if a payment API is down? What if a third-party service is flaky? Network mocking lets you intercept HTTP requests and respond with fake data, eliminating external dependencies and flakiness. Playwright's page.route() API lets you mock API calls, simulate errors, test error handling, and verify that your React app handles network failures gracefully.
What Is Network Mocking and Why Use It?
Network mocking (also called request interception) lets you intercept HTTP requests before they reach the network and respond with fake data. Benefits include: testing error scenarios (500 errors, timeouts, auth failures) that are hard to reproduce in production; isolating your frontend tests from backend changes; testing edge cases (slow networks, large payloads) without deploying special test infrastructure; and speeding up tests by replacing slow API calls with instant fake responses.
Example: you want to test that your app shows an error message when the payment API returns 503 (service unavailable). Rather than taking down the payment API, mock it:
import { test, expect } from '@playwright/test';
test('user sees error when payment API is down', async ({ page }) => {
// Mock the payment API endpoint to return 503
await page.route('**/api/payment/**', route => {
route.abort('serviceunavailable');
});
// User tries to make a payment
await page.goto('/checkout');
await page.locator('[data-testid="pay-button"]').click();
// Expect error message
await expect(page.locator('[data-testid="error-message"]')).toContainText('Payment service is temporarily unavailable');
});
No actual API call was made—Playwright intercepted it and returned an error.
Basic Request Interception with page.route()
page.route(pattern, handler) intercepts requests matching a URL pattern and lets you decide what happens:
import { test, expect } from '@playwright/test';
test('mock successful API response', async ({ page }) => {
// Intercept GET requests to /api/user
await page.route('**/api/user', route => {
// Return a fake response
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: 'John Doe',
email: '[email protected]',
}),
});
});
// Load the page—API calls will return the mocked data
await page.goto('/profile');
// Verify the mocked data is displayed
await expect(page.locator('[data-testid="user-name"]')).toContainText('John Doe');
await expect(page.locator('[data-testid="user-email"]')).toContainText('[email protected]');
});
The mock intercepts the API request before it goes to the server and returns fake JSON.
Mocking Different HTTP Status Codes and Error Scenarios
Test error handling by returning different status codes:
test('user sees error on 401 unauthorized', async ({ page }) => {
await page.route('**/api/user', route => {
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'Unauthorized' }),
});
});
await page.goto('/profile');
// Should redirect to login or show error
await expect(page.locator('[data-testid="error-message"]')).toContainText('Please log in again');
});
test('user sees error on 500 server error', async ({ page }) => {
await page.route('**/api/products', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.goto('/products');
await expect(page.locator('[data-testid="error-message"]')).toContainText('Something went wrong');
});
test('user sees error on network timeout', async ({ page }) => {
// Abort the request (no response ever comes)
await page.route('**/api/products', route => {
route.abort('timedout');
});
await page.goto('/products');
// App should show timeout error
await expect(page.locator('[data-testid="error-message"]')).toContainText('Request timed out');
});
These tests verify that your React error handling works correctly for real error scenarios.
Matching Multiple Routes and Selective Mocking
Mock specific endpoints or routes based on conditions:
test('mock only specific routes', async ({ page }) => {
// Mock the products API but let other requests through
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Product A', price: 29.99 },
{ id: 2, name: 'Product B', price: 49.99 },
]),
});
});
// Mock only DELETE requests to analytics
await page.route('**/api/analytics/**', route => {
if (route.request().method() === 'DELETE') {
route.fulfill({ status: 204 });
} else {
route.continue(); // Let other methods through
}
});
await page.goto('/shop');
// Products API is mocked, so data loads instantly
await expect(page.locator('[data-testid="product"]')).toHaveCount(2);
});
This pattern lets you mock specific endpoints while letting others hit the real backend.
Inspecting Request Headers and Payloads
Before mocking, inspect what the frontend is actually sending:
test('inspect request details and mock based on them', async ({ page }) => {
await page.route('**/api/search', route => {
const request = route.request();
// Log the request details
console.log('Method:', request.method());
console.log('URL:', request.url());
console.log('Headers:', request.headers());
console.log('POST data:', request.postData());
// Mock based on the request
const postData = request.postData();
if (postData?.includes('query=javascript')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
results: [
{ id: 1, title: 'JavaScript tutorial' },
{ id: 2, title: 'JavaScript patterns' },
],
}),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ results: [] }),
});
}
});
await page.goto('/search');
await page.locator('[data-testid="search-input"]').fill('javascript');
await page.locator('role=button[name="Search"]').click();
await expect(page.locator('[data-testid="result"]')).toHaveCount(2);
});
Inspecting requests helps you understand the frontend-backend contract and mock accurately.
Simulating Network Delays and Slow APIs
Test how your app handles slow APIs:
test('loading spinner appears while API is slow', async ({ page }) => {
await page.route('**/api/data', async route => {
// Wait 2 seconds before responding
await new Promise(resolve => setTimeout(resolve, 2000));
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: 'some data' }),
});
});
await page.goto('/dashboard');
// Spinner should appear while loading
const spinner = page.locator('[data-testid="loading-spinner"]');
await expect(spinner).toBeVisible();
// After 2 seconds, data loads and spinner disappears
await expect(spinner).not.toBeVisible({ timeout: 3000 });
await expect(page.locator('[data-testid="data"]')).toContainText('some data');
});
This test catches issues where spinners don't appear or hang indefinitely.
Mocking Third-Party APIs (OAuth, Stripe, etc.)
Mocking is especially useful for third-party integrations:
test('user can complete OAuth login flow', async ({ page }) => {
// Mock the OAuth provider's token endpoint
await page.route('https://oauth.example.com/token', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
access_token: 'mock_token_12345',
token_type: 'Bearer',
expires_in: 3600,
}),
});
});
// Mock the OAuth provider's user info endpoint
await page.route('https://oauth.example.com/userinfo', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'user123',
email: '[email protected]',
name: 'John Doe',
}),
});
});
await page.goto('/login');
await page.locator('role=button[name="Sign in with OAuth"]').click();
// OAuth flow completes with mocked responses
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome, John Doe');
});
test('stripe payment succeeds', async ({ page }) => {
// Mock Stripe API
await page.route('https://api.stripe.com/**', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 'pi_test123',
status: 'succeeded',
amount: 2999,
currency: 'usd',
}),
});
});
await page.goto('/checkout');
await page.locator('[data-testid="card-number"]').fill('4242 4242 4242 4242');
await page.locator('[data-testid="pay-button"]').click();
// Payment succeeds (mocked)
await expect(page.locator('[data-testid="success-message"]')).toContainText('Payment successful');
});
Mocking third-party APIs lets you test integrations without depending on external services or requiring API credentials in tests.
Verifying That Requests Were Made
Assert that the frontend made the correct API calls:
import { test, expect } from '@playwright/test';
test('verify correct API requests are made', async ({ page }) => {
// Track requests
const requests: string[] = [];
await page.route('**/api/**', route => {
requests.push(route.request().url());
// Mock response
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true }),
});
});
await page.goto('/dashboard');
// Verify expected requests were made
expect(requests).toContain(expect.stringContaining('/api/user'));
expect(requests).toContain(expect.stringContaining('/api/dashboard-data'));
// Verify unexpected requests were NOT made
expect(requests).not.toContain(expect.stringContaining('/api/admin'));
});
Request verification catches bugs where the frontend makes unnecessary or incorrect API calls.
Combining Real and Mocked APIs
In practice, you often mock just the problematic or slow APIs and let others hit the real backend:
test('use real auth API but mock payment API', async ({ page }) => {
// Let auth API calls through
// (assuming auth API is fast and reliable)
// Mock payment API (slow or expensive to test)
await page.route('**/api/payment/**', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ transaction_id: 'txn_test123' }),
});
});
// Navigate and test
await page.goto('/checkout');
await page.locator('[data-testid="pay-button"]').click();
await expect(page.locator('[data-testid="success"]')).toBeVisible();
});
This hybrid approach balances test reliability with testing against real systems.
Key Takeaways
- Network mocking intercepts HTTP requests and returns fake responses, eliminating external dependencies and flakiness.
- Use
page.route()to mock specific API endpoints by URL pattern. - Mock error scenarios (500, 401, timeout) to test error handling without bringing down real services.
- Inspect request details (
method(),postData()) to mock responses accurately. - Simulate network delays to test loading states and spinner behavior.
- Mock third-party APIs (OAuth, Stripe) to avoid depending on external services or exposing credentials.
Frequently Asked Questions
Should I mock all APIs or let some hit the real backend?
Mock slow, expensive, or unreliable APIs (payment processing, third-party integrations). Let fast, stable APIs (your own backend) hit the real service if it's in a test environment. This balances speed with confidence.
How do I mock file uploads?
File uploads are typically mocked at the form level using page.locator('input[type="file"]').setInputFiles(). For API responses after upload, mock the upload endpoint:
await page.route('**/api/upload', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ url: 'https://example.com/file.pdf' }),
});
});
Can I mock only certain requests and let others through?
Yes, use route.continue() to let requests through:
await page.route('**/api/**', route => {
if (route.request().url().includes('/user')) {
route.fulfill({ status: 200, body: '{}' });
} else {
route.continue(); // Let request go to real backend
}
});
How do I handle GraphQL mutations and queries?
GraphQL requests are POST to a single endpoint, typically /graphql. Inspect the request body to determine the operation:
await page.route('**/graphql', route => {
const postData = route.request().postData();
if (postData?.includes('GetUser')) {
route.fulfill({
status: 200,
body: JSON.stringify({ data: { user: { id: '1', name: 'John' } } }),
});
}
});
Do mocked responses count against rate limits or API quota?
No, mocked responses are never sent to the real API, so they don't consume API quota or hit rate limits. This is a major benefit of mocking.