Mocking external network requests is a crucial part of testing applications that rely on APIs. Mock Service Worker (MSW) is a powerful tool that allows you to intercept and mock network requests at the network level. When combined with Vitest, a fast and modern JavaScript testing framework, you can create robust and reliable tests for your applications.
This post explores how to use MSW with Vitest to mock API calls effectively. We’ll cover setup, implementation, best practices, and potential pitfalls, helping you understand how to integrate MSW into your testing strategy.
What is Mock Service Worker (MSW)?
MSW is a JavaScript library that lets you mock API requests by intercepting them at the network level. It works in both browser and Node.js environments, allowing you to:
- Mock REST and GraphQL APIs.
- Simulate different server responses, including errors.
- Keep your tests close to real-world scenarios by not altering the actual code making the requests.
Why Use MSW with Vitest?
- Network-Level Mocking: MSW intercepts actual network requests, providing more realistic test conditions.
- Flexibility: Easily mock different responses without changing the code under test.
- Maintainability: Keep your tests clean by avoiding hard-coded mocks or complex mocking setups.
- Consistency: Use the same mocking approach for both development and testing environments.
Setting Up MSW with Vitest
Install MSW
npm install --save-dev msw
Configure MSW in Your Project
Create a src/mocks
directory and set up the handlers for your API endpoints.
Example: Handlers Definition
// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
// Mock a GET request to /api/user
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ id: '123', name: 'John Doe' }));
}),
// Mock a POST request to /api/login
rest.post('/api/login', (req, res, ctx) => {
const { username } = req.body;
return res(ctx.status(200), ctx.json({ message: `Welcome, ${username}!` }));
}),
];
Set Up the Server for Testing
Create a file to initialize the MSW server for testing.
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(…handlers);
Configure Vitest to Use MSW
In your test setup file (e.g., setupTests.js
), start the server before tests run and clean up afterward.
// setupTests.js
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
// Start the server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Reset any request handlers that are declared as a part of our tests
// (i.e. for testing one-time error scenarios)
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished
afterAll(() => server.close());
Update your vitest.config.js
to include the setup file:
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: './setupTests.js',
},
});
Writing Tests with MSW and Vitest
Example: Testing a Component That Fetches Data
Suppose you have a React component that fetches user data:
// UserProfile.jsx
import React, { useEffect, useState } from 'react';
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser);
}, []);
if (!user) return <div>Loading…</div>;
return (
<div>
<h1>{user.name}</h1>
</div>
);
}
Test File:
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { UserProfile } from './UserProfile';
test('renders user profile after fetching data', async () => {
// Render the component
render(<UserProfile />);
// Verify loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for the user data to be displayed
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
});
Explanation:
- The test renders the
UserProfile
component. - It checks for the loading state.
- It uses
findByText
to wait for the asynchronous update. - MSW intercepts the
/api/user
request and returns the mocked response defined inhandlers.js
.
Example: Testing Error Handling
You can mock error responses to test how your component handles failures.
Adjust the Test:
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { rest } from 'msw';
import { server } from '../mocks/server';
import { UserProfile } from './UserProfile';
test('handles server error', async () => {
// Override the default handler for this test
server.use(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(500));
}),
);
render(<UserProfile />);
// Wait for error message
const errorMessage = await screen.findByText(/failed to load user/i);
expect(errorMessage).toBeInTheDocument();
});
Explanation:
server.use(…)
: Overrides the/api/user
handler for this test, simulating a server error.- The component should handle the error and display an appropriate message.
- The test verifies that the error message is displayed.
Best Practices with MSW
Centralize Request Handlers
Define all your request handlers in one place (handlers.js
) to keep your mocks organized.
Benefits:
- Maintainability: Easier to update and manage mock responses.
- Reusability: Handlers can be reused across multiple tests.
Use Contextual Request Handlers for Specific Tests
Override handlers within tests to simulate different scenarios.
server.use(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(404), ctx.json({ error: 'User not found' }));
}),
);
Benefits:
- Flexibility: Test edge cases and error conditions without affecting other tests.
- Isolation: Changes to handlers in one test don’t impact others.
Verify Requests in Tests
You can assert that certain requests were made with expected parameters.
import { expect, test } from 'vitest';
import { rest } from 'msw';
import { server } from '../mocks/server';
test('submits form data correctly', async () => {
let requestBody;
server.use(
rest.post('/api/submit', (req, res, ctx) => {
requestBody = req.body;
return res(ctx.status(200));
}),
);
// … render component and trigger form submission …
expect(requestBody).toEqual({ name: 'Alice', age: 30 });
});
Handle Unhandled Requests
Configure MSW to alert you to unhandled requests, helping catch missing handlers.
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
Options for onUnhandledRequest
:
'bypass'
: Default behavior, unhandled requests proceed as normal.'warn'
: Logs a warning for unhandled requests.'error'
: Throws an error for unhandled requests.
Use TypeScript for Better Type Safety (Optional)
If you’re using TypeScript, MSW provides type definitions to enhance your development experience.
Potential Pitfalls and How to Avoid Them
Forgetting to Reset Handlers
Issue: Overridden handlers persist across tests, causing unexpected behavior.
Solution:
- Use
server.resetHandlers()
inafterEach
to reset handlers after each test.
afterEach(() => server.resetHandlers());
Mocking Incorrectly Formatted Responses
Issue: Returning responses that don’t match the expected format can cause test failures.
Solution:
- Ensure that mocked responses match the shape and structure expected by the code under test.
Ignoring Unhandled Requests
Issue: Missing handlers for API calls can lead to unhandled requests, causing tests to pass when they should fail.
Solution:
- Set
onUnhandledRequest
to'error'
during testing to catch unhandled requests.
Over-Mocking
Issue: Over-mocking can lead to tests that don’t reflect real-world behavior.
Solution:
- Mock only external network requests.
- Avoid mocking internal functions or modules unnecessarily.
Examples in Practice
Testing a Component with Multiple API Calls
// Dashboard.jsx
import React, { useEffect, useState } from 'react';
export function Dashboard() {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser);
fetch('/api/notifications')
.then((res) => res.json())
.then(setNotifications);
}, []);
if (!user || notifications.length === 0) return <div>Loading…</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
</div>
);
}
Handlers:
// handlers.js
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ id: '123', name: 'Alice' }));
}),
rest.get('/api/notifications', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: '1', message: 'Notification 1' },
{ id: '2', message: 'Notification 2' },
]),
);
}),
];
Test:
// Dashboard.test.jsx
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { Dashboard } from './Dashboard';
test('renders user and notifications', async () => {
render(<Dashboard />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const welcomeMessage = await screen.findByText(/welcome, alice/i);
expect(welcomeMessage).toBeInTheDocument();
const notificationItems = await screen.findAllByRole('listitem');
expect(notificationItems).toHaveLength(2);
});
Explanation:
- MSW handles both
/api/user
and/api/notifications
requests. - The test verifies that both user data and notifications are rendered correctly.
Conclusion
Using Mock Service Worker with Vitest provides a powerful and flexible way to mock network requests in your tests. By intercepting requests at the network level, MSW allows you to create realistic and maintainable tests that closely mimic actual application behavior.
Key Takeaways
- MSW Benefits:
- Mocks network requests at the network level.
- Works seamlessly in both browser and Node.js environments.
- Simplifies testing of components that rely on external APIs.
- Best Practices:
- Centralize request handlers for maintainability.
- Use contextual handlers to simulate different scenarios.
- Verify requests and responses within tests.
- Handle unhandled requests to catch missing handlers.
- Avoiding Pitfalls:
- Reset handlers between tests to prevent cross-test contamination.
- Ensure mocked responses match expected formats.
- Be cautious of over-mocking.
By integrating MSW into your testing workflow with Vitest, you can enhance the reliability and robustness of your tests, ultimately leading to higher-quality applications.
When to Use MSW
- API-Dependent Components: When testing components that make HTTP requests to external APIs.
- Error Handling Scenarios: To simulate server errors and test how your application responds.
- Complex Response Structures: When dealing with complex or large API responses that would be cumbersome to mock manually.
Potential Limitations
- Learning Curve: Requires understanding of how MSW intercepts requests and how to configure handlers.
- Setup Overhead: Initial setup may be more involved compared to simple mocking, but pays off in the long run.