When to Use Proxy vs Inline Mocking
Choosing the wrong interception layer produces one of two failure modes: a unit suite that passes locally but fails in CI because it never validates real network behaviour, or an integration pipeline that takes twelve minutes to run because every test round-trips through a socket. This page resolves that decision with concrete thresholds — not rules of thumb.
Context: where the two strategies diverge
Both approaches serve the same goal — replacing a real upstream API with controlled responses — but they intercept requests at fundamentally different points in the stack.
Proxy vs inline mocking strategies diverge at the transport boundary. A proxy sits between the network adapter and the upstream server: the application makes a genuine TCP connection, which the proxy intercepts. An inline mock replaces the network call itself inside the JavaScript runtime — fetch, XMLHttpRequest, or an HTTP client instance — so no socket is ever opened.
The environment and test-suite characteristics that make one choice correct make the other wrong.
Solution: five decision thresholds
Work through these in order. The first threshold that resolves conclusively determines your choice for that layer.
1. CORS and pre-flight validation
If the environment enforces strict origin headers during local development — for example, a browser integration test that must exercise the OPTIONS pre-flight and verify Access-Control-Allow-Origin — proxy mocking is the only viable option. The request interception pattern that inline mocks use bypasses the browser’s network stack entirely, so pre-flight requests are never sent and CORS cannot be validated.
2. Test-suite size and CI budget
Inline mocks execute in under 2 ms per request because no socket is involved. Proxy mocks add 15–50 ms due to localhost socket routing. At 500 or more HTTP-dependent tests, inline mocking reduces CI runtime by 40–60%. If your suite is smaller or if realistic latency is a test concern, proxy mocking’s overhead is negligible.
3. State mutation across sequential requests
If mocks must carry state across a sequence — a POST /orders creates a record and a subsequent GET /orders/:id must return it — use a stateful proxy server such as WireMock with scenario state. Inline mocks can replicate this but require explicit manual state management between handler invocations, which becomes brittle as sequences lengthen. The mock lifecycle management principles that govern startup, state reset, and teardown apply most naturally to proxy-level servers.
4. Framework coupling and tool reuse
Inline mocks bind to a specific test runner (Jest, Vitest) and HTTP client. A proxy mock is framework-agnostic: Playwright, Cypress, Postman, and a shell script can all issue requests against the same running WireMock or Prism instance without any runner-specific configuration. Platform teams standardising mock routing across polyglot services should favour the proxy layer.
5. Silent failure diagnosis
Proxy mocks fail silently in specific patterns: changeOrigin: true missing causes host-header rejection; the bypass function accidentally intercepting WebSocket upgrade requests drops long-polling connections; and misconfigured X-Forwarded-For headers cause authentication middleware to reject requests. Inline mock failures are immediate and visible — a missing handler throws synchronously into the test output.
Step-by-step implementation
Proxy mocking with Vite dev server
This configuration proxies /api/v2 to a local Prism or WireMock instance and uses a bypass function to exclude health-check endpoints:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api/v2': {
target: 'http://localhost:4010',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v2/, ''),
bypass(req) {
// Exclude health checks from mock routing
if (req.url === '/api/v2/health') return req.url;
// Pass X-Mock-Scenario header through to WireMock
req.headers['x-mock-scenario'] = process.env.MOCK_SCENARIO ?? 'default';
}
}
}
}
});
Start the mock server before the dev server:
# Start WireMock on port 4010
docker run --rm -p 4010:8080 \
-v "$(pwd)/wiremock:/home/wiremock" \
wiremock/wiremock:3.3.1 \
--global-response-templating \
--stateful-scenarios
Inline mocking with Vitest and axios-mock-adapter
This setup guarantees zero state leakage between parallel test workers by constructing and restoring the adapter inside each test lifecycle:
// __tests__/setup/axios-mock.ts
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { beforeEach, afterEach } from 'vitest';
let mockAdapter: MockAdapter;
beforeEach(() => {
mockAdapter = new MockAdapter(axios, { onNoMatch: 'throwException' });
});
afterEach(() => {
mockAdapter.restore();
});
export function getMockAdapter(): MockAdapter {
return mockAdapter;
}
Register it in vitest.config.ts:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['__tests__/setup/axios-mock.ts'],
pool: 'forks', // isolate module registries between workers
}
});
MSW as a hybrid layer
MSW handler registration bridges both strategies: setupWorker operates at the Service Worker layer (network-level, browser context) while setupServer intercepts at the Node runtime layer (inline, no socket). Use setupWorker for Playwright integration tests that need realistic network behaviour, and setupServer for Vitest unit suites.
// src/mocks/server.ts — Node/Vitest inline variant
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/mocks/browser.ts — Service Worker proxy-like variant
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
The handlers array is shared — this is the critical advantage: a single source of truth for mock definitions used by both layers.
Verification
After configuring either layer, confirm correct interception with a targeted assertion:
# Proxy: verify the mock server is receiving routed requests
curl -s -o /dev/null -w "%{http_code}" \
-H "x-mock-scenario: default" \
http://localhost:5173/api/v2/users/1
# Expected: 200 (from WireMock stub, not upstream)
# Inline: verify no real network calls escape the test suite
VITEST_POOL=forks npx vitest run --reporter=verbose 2>&1 | grep "Network request"
# Expected: zero lines (no unhandled network requests)
For proxy mocking, inspect the WireMock journal to confirm requests are being matched and not falling through to unmatched-request errors:
curl -s http://localhost:4010/__admin/requests | jq '.requests | length'
# Should increment with each test run
Gotchas and edge cases
-
WebSocket upgrade interception. Vite’s proxy
bypassfunction receives WebSocket upgrade requests. If the function returnsundefinedfor those requests, Vite attempts to proxy them to the mock server, which typically does not speak the WebSocket protocol. Add an explicit check:if (req.headers.upgrade === 'websocket') return req.url;to pass WebSocket traffic through directly. -
Inline mock order sensitivity. When using MSW’s
server.use()to override handlers inside a test, the override is prepended to the handler list — the first matching handler wins. If a previousserver.use()call from a test that ran earlier was not cleaned up (missingserver.resetHandlers()), it silently takes precedence over your new handler. Always callserver.resetHandlers()inafterEach, not justafterAll. -
Parallel worker module isolation. With Vitest’s
--pool=threads, worker threads share a module registry by default. A singleton axios instance imported at module scope is shared between tests in different files. Switch to--pool=forksor restructure the client as a factory function to guarantee per-test isolation when using inline mocks.
Related
- Proxy vs Inline Mocking Strategies — the architectural trade-offs between the two layers in depth
- How to Intercept Fetch Requests in React — applying inline interception patterns specifically in React component tests
- Running WireMock in Docker Compose — lifecycle management for proxy-layer mock servers in CI
- Writing Custom MSW Response Resolvers — advanced handler patterns for the hybrid inline/Service Worker approach
← Back to Proxy vs Inline Mocking Strategies