4.9 KiB

Mock Service Worker (MSW) Guide

Overview

Mock Service Worker (MSW) is an API mocking library that intercepts outgoing network requests at the network level. Unlike traditional mocking that patches fetch or axios, MSW uses a Service Worker in the browser and direct request interception in Node.js—making mocks transparent to your application code.

We use MSW in this project for:

  • Testing: Write reliable unit and integration tests without real network calls
  • Development: Run the frontend with mocked APIs when the backend isn't available or when working on features with pending backend APIs

The same mock handlers work in both environments, so you write them once and reuse everywhere.

Relevant Files

  • src/mocks/handlers.ts - Main handler registry that combines all domain handlers
  • src/mocks/*-handlers.ts - Domain-specific handlers (auth, billing, conversation, etc.)
  • src/mocks/browser.ts - Browser setup for development mode
  • src/mocks/node.ts - Node.js setup for tests
  • vitest.setup.ts - Global test setup with MSW lifecycle hooks

Development Workflow

Running with Mocked APIs

# Run with API mocking enabled
npm run dev:mock

# Run with API mocking + SaaS mode simulation
npm run dev:mock:saas

These commands set VITE_MOCK_API=true which activates the MSW Service Worker to intercept requests.

Note

OSS vs SaaS Mode

OpenHands runs in two modes:

  • OSS mode: For local/self-hosted deployments where users provide their own LLM API keys and configure git providers manually
  • SaaS mode: For the cloud offering with billing, managed API keys, and OAuth-based GitHub integration

Use dev:mock:saas when working on SaaS-specific features like billing, API key management, or subscription flows.

Writing Tests

For most tests, mock at the service layer using vi.spyOn. This approach is explicit, test-scoped, and makes the scenario being tested clear.

import { vi } from "vitest";
import SettingsService from "#/api/settings-service/settings-service.api";

const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
getSettingsSpy.mockResolvedValue({
  llm_model: "openai/gpt-4o",
  llm_api_key_set: true,
  // ... other settings
});

Use mockResolvedValue for success scenarios and mockRejectedValue for error scenarios:

getSettingsSpy.mockRejectedValue(new Error("Failed to fetch settings"));

Network Layer Mocking (Advanced)

For tests that need actual network-level behavior (WebSockets, testing retry logic, etc.), use server.use() to override handlers per test.

Important

Reuse the global server instance - Don't create new setupServer() calls in individual tests. The project already has a global MSW server configured in vitest.setup.ts that handles lifecycle (server.listen(), server.resetHandlers(), server.close()). Use server.use() to add runtime handlers for specific test scenarios.

import { http, HttpResponse } from "msw";
import { server } from "#/mocks/node";

it("should handle server errors", async () => {
  server.use(
    http.get("/api/my-endpoint", () => {
      return new HttpResponse(null, { status: 500 });
    }),
  );
  // ... test code
});

For WebSocket testing, see __tests__/helpers/msw-websocket-setup.ts for utilities.

Adding New API Mocks

When adding new API endpoints, create mocks in both places to maintain 1:1 similarity with the backend:

1. Add to src/mocks/ (for development)

Create or update a domain-specific handler file:

// src/mocks/my-feature-handlers.ts
import { http, HttpResponse } from "msw";

export const MY_FEATURE_HANDLERS = [
  http.get("/api/my-feature", () => {
    return HttpResponse.json({
      data: "mock response",
    });
  }),
];

Register in handlers.ts:

import { MY_FEATURE_HANDLERS } from "./my-feature-handlers";

export const handlers = [
  // ... existing handlers
  ...MY_FEATURE_HANDLERS,
];

2. Mock in tests for specific scenarios

In your test files, spy on the service method to control responses per test case:

import { vi } from "vitest";
import MyFeatureService from "#/api/my-feature-service.api";

const spy = vi.spyOn(MyFeatureService, "getData");
spy.mockResolvedValue({ data: "test-specific response" });

See __tests__/routes/llm-settings.test.tsx for a real-world example of service layer mocking.

Tip

For guidance on creating service APIs, see src/api/README.md.

Best Practices

  • Keep mocks close to real API contracts - Update mocks when backend changes
  • Use service layer mocking for most tests - It's simpler and more explicit
  • Reserve network layer mocking for integration tests - WebSockets, retry logic, etc.
  • Export mock data from handler files - Reuse in tests (e.g., MOCK_DEFAULT_USER_SETTINGS)