Introduction
Modern web applications depend heavily on APIs.
A single checkout page may call services for:
- Product information
- Inventory
- Pricing
- Promotions
- Customer details
- Payments
- Recommendations
Testing all of those services through real environments can make UI automation slow, unreliable, and difficult to control.
This is where playwright network mocking advanced techniques become valuable.
Playwright provides network interception APIs that allow tests to observe, modify, fulfill, or abort network requests. The primary API is page.route(), which can intercept matching requests before they reach the server. (playwright.dev)
A simple example is:
await page.route(‘**/api/products’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: []
})
});
});
Now the browser receives controlled data without calling the real product API.
But enterprise Playwright Network Mocking requires much more than replacing one response.
A mature strategy needs to handle:
- Request interception
- Response mocking
- Request modification
- Response modification
- HTTP failures
- Network delays
- Authentication
- GraphQL
- Dynamic test data
- Conditional routing
- Page Object Model integration
- Custom fixtures
- Parallel execution
- CI/CD
- Debugging
- Test isolation
This playwright network mocking advanced tutorial explains these techniques with practical Playwright TypeScript examples.
What Is Playwright Network Mocking?
Network mocking means replacing a real network dependency with a controlled test response.
The basic flow is:
Browser
|
| HTTP request
↓
page.route()
|
+—- Mock → route.fulfill()
|
+—- Modify → route.fetch()
|
+—- Continue → route.continue()
|
+—- Stop → route.abort()
This allows you to test application behavior without depending on the real backend.
For example:
await page.route(‘**/api/orders’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
orders: []
})
});
});
The UI behaves as if the server returned an empty order list.
Network Mocking vs Network Interception vs API Testing
These terms are related but not identical.
| Technique | Purpose | Real backend called? |
| Network mocking | Replace server response | Usually no |
| Network interception | Observe/modify request or response | Sometimes |
| API testing | Test API directly | Yes |
| Stubbing | Replace dependency with predefined behavior | Usually no |
| Virtualization | Simulate unavailable external system | No |
| Contract testing | Verify service contracts | Usually service-level |
When to mock
Mock when you need:
- Deterministic data
- Rare error conditions
- Unavailable third-party services
- Fast UI tests
- Edge-case responses
- Repeatable scenarios
When to use real APIs
Use real APIs when validating:
- Backend integration
- API contracts
- Authentication integration
- Database behavior
- End-to-end business flows
A strong Playwright Testing Framework uses both approaches.
Why Use Network Mocking in Automation Testing?
Consider a payment test.
The payment provider may:
- Take several seconds
- Have rate limits
- Require special test accounts
- Return unpredictable responses
- Be unavailable in CI
Instead of depending on it for every UI test, simulate the payment response.
This makes the test:
Fast
+
Deterministic
+
Repeatable
+
CI-friendly
Network mocking is especially useful for negative testing.
For example:
200 → Success
400 → Invalid request
401 → Unauthenticated
403 → Unauthorized
404 → Not found
500 → Server failure
503 → Service unavailable
You can test all these states without manipulating the real backend.
Playwright page.route() Fundamentals
The central API is:
await page.route(url, handler);
Example:
import { test, expect } from ‘@playwright/test’;
test(‘mock products API’, async ({ page }) => {
await page.route(‘**/api/products’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: [
{
id: 1,
name: ‘Laptop’,
price: 1000
}
]
})
});
});
await page.goto(‘/products’);
await expect(
page.getByText(‘Laptop’)
).toBeVisible();
});
Real API → Interception → Mock → Application → Assertion
/api/products
↓
page.route()
↓
route.fulfill()
↓
Browser receives JSON
↓
UI renders Laptop
↓
Playwright assertion
The route pattern can use glob patterns, regular expressions, or other supported URL matching approaches. (playwright.dev)
Mocking API Responses With route.fulfill()
route.fulfill() completes the request using a response you provide.
await page.route(‘**/api/profile’, async route => {
await route.fulfill({
status: 200,
headers: {
‘content-type’: ‘application/json’
},
body: JSON.stringify({
id: 101,
name: ‘Automation User’,
role: ‘QA Engineer’
})
});
});
Problem
The profile API is unstable in the test environment.
Mocking Strategy
Replace the response with deterministic JSON.
Expected Result
The application receives the expected profile.
Best Practice
Keep mock payloads close to the contract expected by the application.
Do not create unrealistic mock objects that could allow broken application code to pass.
Using route.continue()
Sometimes you want to inspect or modify a request but still send it to the real server.
await page.route(‘**/api/products’, async route => {
await route.continue();
});
This is useful when a route needs conditional processing.
You can modify headers:
await page.route(‘**/api/**’, async route => {
await route.continue({
headers: {
…route.request().headers(),
‘x-test-run’: ‘playwright’
}
});
});
route.continue() sends the request onward without waiting for a response. (playwright.dev)
Using route.fetch() to Modify Real API Responses
This is one of the most useful advanced Playwright network mocking techniques.
Instead of replacing the API completely, fetch the real response and modify it.
await page.route(‘**/api/products’, async route => {
const response = await route.fetch();
const json = await response.json();
json.products.push({
id: 999,
name: ‘Mocked Product’,
price: 1
});
await route.fulfill({
response,
json
});
});
The flow becomes:
Real API
↓
route.fetch()
↓
Original response
↓
Modify JSON
↓
route.fulfill()
↓
Application
This is useful when you want to preserve most of the real backend behavior while controlling a specific part of the response.
Playwright documents route.fetch() as a way to fetch the original response and then use it with route.fulfill() for response modification. (playwright.dev)
Blocking or Aborting Network Requests
Some external resources should never load during a test.
For example:
await page.route(‘**/*.{png,jpg,jpeg,gif}’, async route => {
await route.abort();
});
You can also abort a specific third-party domain:
await page.route(‘**/analytics/**’, async route => {
await route.abort();
});
This can improve test speed and prevent unwanted external dependencies.
For intentional network failures:
await page.route(‘**/api/payment’, async route => {
await route.abort(‘failed’);
});
Playwright supports abort error codes such as failed, timedout, and other network-level errors. (playwright.dev)
Mocking Dynamic API Responses and Test Data
Hard-coded responses are useful, but advanced suites often need generated data.
function createProduct(id: number) {
return {
id,
name: `Product ${id}`,
price: id * 10,
available: true
};
}
await page.route(‘**/api/products’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: [
createProduct(1),
createProduct(2),
createProduct(3)
]
})
});
});
This makes it easier to test:
- One product
- Many products
- Zero products
- Expensive products
- Out-of-stock products
- Boundary values
For enterprise automation, consider separating data factories from route handlers:
tests/
├── mocks/
│ ├── product.mock.ts
│ ├── user.mock.ts
│ └── order.mock.ts
│
├── fixtures/
│ └── network.fixture.ts
│
└── tests/
Simulating HTTP 400, 401, 403, 404, and 500 Errors
Negative testing becomes straightforward.
400 Bad Request
await page.route(‘**/api/orders’, async route => {
await route.fulfill({
status: 400,
contentType: ‘application/json’,
body: JSON.stringify({
error: ‘Invalid order’
})
});
});
401 Unauthorized
await page.route(‘**/api/profile’, async route => {
await route.fulfill({
status: 401,
body: JSON.stringify({
error: ‘Unauthorized’
}),
contentType: ‘application/json’
});
});
403 Forbidden
await page.route(‘**/api/admin/**’, async route => {
await route.fulfill({
status: 403,
body: JSON.stringify({
error: ‘Access denied’
}),
contentType: ‘application/json’
});
});
404 Not Found
await page.route(‘**/api/orders/999’, async route => {
await route.fulfill({
status: 404,
body: JSON.stringify({
error: ‘Order not found’
}),
contentType: ‘application/json’
});
});
500 Server Error
await page.route(‘**/api/payment’, async route => {
await route.fulfill({
status: 500,
body: JSON.stringify({
error: ‘Payment service unavailable’
}),
contentType: ‘application/json’
});
});
These scenarios are ideal for validating UI behavior without manipulating a real backend.
Simulating Network Delays and Slow APIs
A slow API can expose loading-state bugs.
Use:
await page.route(‘**/api/products’, async route => {
await new Promise(resolve =>
setTimeout(resolve, 5000)
);
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: []
})
});
});
Now the UI should display its loading state.
Test:
await expect(
page.getByText(‘Loading products…’)
).toBeVisible();
Then wait for completion:
await expect(
page.getByText(‘No products found’)
).toBeVisible();
Real-world use cases
Simulate:
- 2-second API
- 10-second API
- Timeout behavior
- Slow payment service
- Slow search
- Slow recommendation service
Do not make every test artificially slow. Keep these scenarios targeted.
Mocking Authentication and Authorization Responses
Authentication is a common advanced use case.
Suppose the frontend calls:
POST /api/login
await page.route(‘**/api/login’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
token: ‘mock-token’,
user: {
id: 1,
name: ‘Test Admin’,
role: ‘admin’
}
})
});
});
For an expired session:
await page.route(‘**/api/profile’, async route => {
await route.fulfill({
status: 401,
contentType: ‘application/json’,
body: JSON.stringify({
message: ‘Session expired’
})
});
});
Then assert the application redirects:
await expect(page).toHaveURL(/login/);
Important
Network mocking should test the frontend’s response to authentication conditions.
It should not replace your real authentication integration suite.
Maintain separate tests for real authentication.
Advanced Request Interception and Request Modification
You can inspect requests:
await page.route(‘**/api/**’, async route => {
const request = route.request();
console.log({
method: request.method(),
url: request.url(),
headers: request.headers()
});
await route.continue();
});
Modify query parameters:
await page.route(‘**/api/search**’, async route => {
const url = new URL(route.request().url());
url.searchParams.set(‘pageSize’, ‘100’);
await route.continue({
url: url.toString()
});
});
Modify POST data:
await page.route(‘**/api/orders’, async route => {
const body = route.request().postDataJSON();
body.currency = ‘USD’;
await route.continue({
postData: JSON.stringify(body)
});
});
This allows you to validate how the frontend behaves under controlled request conditions.
GraphQL Request Mocking
GraphQL usually sends requests to one endpoint such as:
POST /graphql
Therefore, inspect the request body to identify the operation.
await page.route(‘**/graphql’, async route => {
const body = route.request().postDataJSON();
if (body.operationName === ‘GetProducts’) {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
data: {
products: [
{
id: ‘1’,
name: ‘Mock Product’,
price: 99
}
]
}
})
});
return;
}
await route.continue();
});
The flow is:
GraphQL request
↓
Read operationName
↓
GetProducts?
↓ ↓
Yes No
↓ ↓
Mock Continue
This is a strong Playwright network mocking advanced example because GraphQL often multiplexes multiple operations through one URL.
Conditional Mocking Based on URL, Method, Headers, or Body
A sophisticated route handler can make decisions based on request metadata.
await page.route(‘**/api/**’, async route => {
const request = route.request();
if (
request.method() === ‘GET’ &&
request.url().includes(‘/products’)
) {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: []
})
});
return;
}
if (
request.method() === ‘POST’ &&
request.url().includes(‘/orders’)
) {
await route.fulfill({
status: 201,
contentType: ‘application/json’,
body: JSON.stringify({
id: ‘mock-order-123’
})
});
return;
}
await route.continue();
});
This approach supports multiple API scenarios without creating separate test files for every response.
Network Mocking With Page Object Model and Custom Fixtures
Network mocking should not make test cases huge.
Create a reusable mock service:
import { Page } from ‘@playwright/test’;
export class ProductMocks {
constructor(private readonly page: Page) {}
async mockEmptyProducts() {
await this.page.route(‘**/api/products’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
products: []
})
});
});
}
async mockServerError() {
await this.page.route(‘**/api/products’, async route => {
await route.fulfill({
status: 500,
contentType: ‘application/json’,
body: JSON.stringify({
error: ‘Server error’
})
});
});
}
}
Then create a fixture:
import { test as base } from ‘@playwright/test’;
import { ProductMocks } from ‘../mocks/product-mocks’;
type Fixtures = {
productMocks: ProductMocks;
};
export const test = base.extend<Fixtures>({
productMocks: async ({ page }, use) => {
await use(new ProductMocks(page));
}
});
Test:
test(’empty product state’, async ({
page,
productMocks
}) => {
await productMocks.mockEmptyProducts();
await page.goto(‘/products’);
await expect(
page.getByText(‘No products found’)
).toBeVisible();
});
Architecture:
Test
↓
Fixture
↓
Mock Service
↓
page.route()
↓
Application
This keeps test code readable.
Network Mocking in Parallel Execution and CI/CD
Network mocks are naturally useful for parallel execution because they reduce external dependencies.
However, route handlers should be test-local.
Prefer:
test(‘scenario’, async ({ page }) => {
await page.route(…);
});
instead of global mutable route state.
Each Playwright test receives its own browser context, which helps isolate network behavior.
For CI:
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
use: {
baseURL: process.env.BASE_URL,
trace: ‘retain-on-failure’
}
});
Network mocking can reduce:
- External API failures
- CI latency
- Third-party rate limits
- Dependency on staging data
But do not mock every API in every test.
Keep a balanced test pyramid:
Many fast mocked UI tests
↓
Fewer real integration tests
↓
Focused API tests
↓
Critical end-to-end flows
Debugging Mocked Network Requests With Logs, Traces, and Reports
Mock failures can be difficult to diagnose if the test does not expose what happened.
Log requests:
await page.route(‘**/api/**’, async route => {
console.log(
‘Intercepted:’,
route.request().method(),
route.request().url()
);
await route.continue();
});
Observe responses:
page.on(‘response’, response => {
if (response.url().includes(‘/api/’)) {
console.log(
response.status(),
response.url()
);
}
});
For failures, enable:
use: {
trace: ‘retain-on-failure’,
screenshot: ‘only-on-failure’,
video: ‘retain-on-failure’
}
Trace Viewer can reveal network activity, request timing, console messages, screenshots, and other test execution information.
When debugging, ask:
- Did the route pattern match?
- Was the request method expected?
- Did another route intercept first?
- Did the mock response contain the correct JSON?
- Did the application make another API call?
- Was authentication required?
- Did the UI receive the expected status code?
Real-World E-Commerce Network Mocking Project
Imagine an e-commerce checkout:
Product API
↓
Inventory API
↓
Cart API
↓
Promotion API
↓
Payment API
Testing every UI scenario against real services is expensive.
Instead, mock controlled conditions.
Out-of-stock product
await page.route(‘**/api/inventory/*’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
available: false,
quantity: 0
})
});
});
Then verify:
await expect(
page.getByText(‘Out of stock’)
).toBeVisible();
Payment failure
await page.route(‘**/api/payment’, async route => {
await route.fulfill({
status: 402,
contentType: ‘application/json’,
body: JSON.stringify({
error: ‘Payment declined’
})
});
});
Slow promotion service
await page.route(‘**/api/promotions’, async route => {
await new Promise(resolve =>
setTimeout(resolve, 3000)
);
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
discount: 10
})
});
});
Now the team can test difficult scenarios deterministically.
Common Playwright Network Mocking Errors and Solutions
| Problem | Likely Cause | Solution |
| Mock never executes | Route pattern doesn’t match | Log request URL |
| Real API called | Incorrect glob | Verify exact URL |
| Wrong response | Multiple handlers | Simplify routing |
| POST mock fails | Incorrect body matching | Inspect postDataJSON() |
| GraphQL mock fails | Wrong operation name | Inspect request body |
| Test hangs | Route never fulfilled/continued | Ensure every branch ends |
| Auth mock fails | Wrong endpoint | Inspect login request |
| Parallel tests interfere | Shared route state | Keep routes test-scoped |
| CI behaves differently | Environment URL differs | Use BASE_URL |
| Mocked UI behaves incorrectly | Unrealistic payload | Match actual API contract |
A particularly dangerous mistake is:
await page.route(‘**/api/**’, async route => {
// no fulfill
// no continue
});
The request is intercepted but never completed.
Always ensure the route handler reaches one of:
await route.fulfill(…);
await route.continue(…);
await route.abort(…);
Advanced Playwright Network Mocking Best Practices
1. Mock selectively
Mock unstable or expensive dependencies.
Do not mock everything.
2. Keep real integration coverage
A fully mocked suite can hide backend integration failures.
3. Match realistic API contracts
Mock payloads should resemble production responses.
4. Centralize reusable mocks
Use mock classes, data factories, or fixtures.
5. Keep routes test-scoped
Avoid global mutable network state.
6. Use route.fetch() when appropriate
It is ideal when you need the real response with a controlled modification.
7. Test negative scenarios deliberately
Mock:
- 400
- 401
- 403
- 404
- 409
- 429
- 500
- 503
8. Test loading states
Network delays are useful for verifying spinners, skeletons, and timeout handling.
9. Log important routes
Do not overwhelm CI logs with every static asset.
10. Keep test data deterministic
Avoid random mock responses unless randomness itself is being tested.
11. Design for parallel execution
Every test should be able to run independently.
12. Separate mock logic from assertions
Mocks prepare the environment.
Tests verify behavior.
13. Keep authentication tests separate
Mocked authentication is useful for frontend scenarios, but real authentication integration must also be tested.
14. Treat mocks as test code
Review them just like application code.
Incorrect mocks can produce false confidence.
Advanced Playwright Network Mocking Interview Questions With Answers
1. What is Playwright network mocking?
It is the controlled interception and replacement of browser network requests or responses during testing.
2. Which Playwright API is commonly used for network interception?
page.route().
await page.route(‘**/api/users’, async route => {
await route.fulfill(…);
});
3. What is route.fulfill()?
It completes the intercepted request with a response controlled by the test.
4. What is route.continue()?
It allows the request to proceed to the real destination, optionally with modified request properties.
5. What is route.fetch()?
It fetches the original response, allowing the test to inspect or modify it before calling route.fulfill().
6. How do you simulate a 500 error?
await route.fulfill({
status: 500,
body: JSON.stringify({
error: ‘Server failure’
})
});
7. How do you simulate a network failure?
Use:
await route.abort(‘failed’);
8. How do you mock GraphQL?
Intercept the GraphQL endpoint and inspect request().postDataJSON() to determine the operationName.
9. Should all API calls be mocked?
No. Use mocks for deterministic UI scenarios while retaining real API and integration coverage.
10. How do you design network mocks for an enterprise framework?
Separate:
Mock data factories
↓
Mock services
↓
Custom fixtures
↓
Tests
This provides reusable and maintainable network simulation.
11. How do you prevent mock behavior from leaking between tests?
Keep routes scoped to the test’s page/context and avoid global mutable state.
12. How would you debug a route that does not match?
Log:
console.log(route.request().url());
console.log(route.request().method());
Then compare the actual URL and method against the route pattern.
Playwright Network Mocking Learning Roadmap
If you are moving from Selenium to Playwright, learn network mocking after understanding browser contexts and Playwright locators.
Level 1 — Playwright Fundamentals
Learn:
- TypeScript
- Locators
- Assertions
- Page
- BrowserContext
- Test configuration
Level 2 — Network Fundamentals
Learn:
- Requests
- Responses
- HTTP methods
- Status codes
- Headers
- JSON
Level 3 — Playwright Network Mocking
Learn:
- page.route()
- route.fulfill()
- route.continue()
- route.abort()
- route.fetch()
Level 4 — Advanced Scenarios
Learn:
- Authentication
- GraphQL
- Request modification
- Response modification
- Network failures
- Delays
- Dynamic data
- Conditional routing
Level 5 — Enterprise Framework Design
Learn:
- Custom fixtures
- Page Object Model
- API clients
- Mock services
- Parallel execution
- CI/CD
- Test isolation
- Reporting
- Debugging
Related topics include Advanced Playwright Automation Techniques, Playwright Custom Fixtures Advanced, Playwright Test Sharding Advanced, Playwright Visual Regression Advanced Setup, Playwright API Testing, Playwright API Authentication, Playwright Network Interception, Playwright Data Driven Testing, Playwright Authentication Tutorial, Playwright Page Object Model, Playwright Fixtures Tutorial, Playwright Parallel Execution Tutorial, Playwright Reporting Tutorial, Playwright CI/CD Tutorial, Playwright GitHub Actions Tutorial, Playwright Docker Tutorial, Playwright TypeScript Tutorial, Playwright Best Practices, and Playwright Interview Questions.
FAQs: Playwright Network Mocking Advanced
What is advanced Playwright network mocking?
Advanced Playwright network mocking uses request interception to simulate APIs, modify real responses, inject failures, introduce delays, control authentication, mock GraphQL, generate deterministic data, and support isolated CI testing.
How do I mock an API response in Playwright?
Use page.route() with route.fulfill():
await page.route(‘**/api/users’, async route => {
await route.fulfill({
status: 200,
contentType: ‘application/json’,
body: JSON.stringify({
users: []
})
});
});
How do I intercept requests in Playwright?
Use:
await page.route(‘**/api/**’, async route => {
await route.continue();
});
What is the difference between route.fulfill() and route.continue()?
route.fulfill() provides a response directly. route.continue() allows the original request to proceed to the server.
When should I use route.fetch()?
Use route.fetch() when you want to call the real API but modify its response before the browser receives it.
Can Playwright mock HTTP 500 errors?
Yes:
await route.fulfill({
status: 500,
body: JSON.stringify({
error: ‘Internal server error’
})
});
Can Playwright simulate slow network responses?
Yes. You can delay the route handler before calling route.fulfill().
Can Playwright mock GraphQL?
Yes. Intercept the GraphQL endpoint and inspect the operation name or request body to return operation-specific responses.
Is network mocking better than real API testing?
Neither replaces the other. Mocking provides deterministic UI testing, while real API testing validates backend and integration behavior.
How does network mocking improve CI/CD?
It reduces dependency on unstable external services, makes edge cases reproducible, reduces test execution time, and improves test isolation.
Can Playwright network mocks run in parallel?
Yes. Test-scoped routing works well with Playwright’s isolated browser contexts. Avoid shared mutable mock state.
