If you've ever been in a situation where the API for a feature you're adding to your frontend is not ready, then MSW is for you.
At work, this is often the case for me! I have been using MSW for integration testing but I never used it for developing because I was under the impression that it would intercept and reject any requests that weren't mocked.
I was wrong.
It doesn't reject unmocked requests, it just passes it along.
I thought back to Kent C. Dodd's post on dev tools and knew that dynamically mocking API's would really speed up my development workflow (it did).
Here's how I did it.
Making sure it's dev only
// App.tsx
const DevTools = React.lazy(() => import("./DevTools"));
function App() {
return (
<>
<Routes />
{process.env.NODE_ENV === "development" ? (
<React.Suspense fallback={null}>
<DevTools />
</React.Suspense>
) : null}
</>
);
}
Tada! Haha it would be great if that was all, but this is how I made sure the dev tools only loaded within the development environment. A simple dynamic component with a null suspense fallback.
This is the actual DevTools.tsx implementation:
// DevTools.tsx
import * as React from "react";
import { setupWorker, graphql } from "msw";
export const mockServer = setupWorker();
const mocks = {
users: [
graphql.query("GetUsers", (req, res, ctx) => {
// return fake data
}),
graphql.query("GetUser", (req, res, ctx) => {
// return fake data
}),
],
};
function DevTools() {
const [mocks, setMocks] = React.useState({});
const mockServerReady = React.useRef(false);
const activeMocks = React.useMemo(
() =>
Object.entries(mocks)
// we filter out all unchecked inputs
.filter(([, shouldMock]) => shouldMock)
// since the map is an array of handlers
// we want to flatten the array so that the final result isn't nested
.flatMap(([key]) => mocks[key]),
[mocks]
);
React.useEffect(() => {
mockServer.start().then(() => {
mockServerReady.current = true;
});
return () => {
mockServer.resetHandlers();
mockServer.stop();
};
}, []);
React.useEffect(() => {
if (mockServerReady.current) {
flushMockServerHandlers();
}
}, [state.mock]);
// if a checkbox was unchecked
// we want to make sure that the mock server is no longer mocking those API's
// we reset all the handlers
// then add them to MSW
function flushMockServerHandlers() {
mockServer.resetHandlers();
addHandlersToMockServer(activeMocks);
}
function addHandlersToMockServer(handlers) {
mockServer.use(...handlers);
}
function getInputProps(name: string) {
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
const apiToMock = event.target.name;
const shouldMock = event.target.checked;
setState((prevState) => ({
...prevState,
[apiToMock]: shouldMock,
}));
}
return {
name,
onChange,
checked: state.mock[name] ?? false,
};
}
return (
<div>
{Object.keys(mocks).map((mockKey) => (
<div key={mockKey}>
<label htmlFor={mockKey}>Mock {mockKey}</label>
<input {...getInputProps(mockKey)} />
</div>
))}
</div>
);
}
Let's break that down.
Mock server
Inside the DevTools.tsx file, I initialize the mock server and I add a map of all the API's I want to be able to mock and assign it to mocks. In this example I'm using graphql, but you could easily replace that with whatever REST API you may be using.
// DevTools.tsx
import { setupWorker, graphql } from "msw";
export const mockServer = setupWorker();
const mocks = {
users: [
graphql.query("GetUsers", (req, res, ctx) => {
// return fake data
}),
graphql.query("GetUser", (req, res, ctx) => {
// return fake data
}),
],
};
UI
I make a checkbox for every key within mocks.
The getInputProps initializes all the props for each checkbox. Each time a checkbox is checked, I'll update the state to reflect which API should be mocked.
// DevTools.tsx
function DevTools() {
const [mocks, setMocks] = React.useState({});
function getInputProps(name: string) {
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
const apiToMock = event.target.name;
const shouldMock = event.target.checked;
setState((prevState) => ({
...prevState,
[apiToMock]: shouldMock,
}));
}
return {
name,
onChange,
checked: state.mock[name] ?? false,
};
}
return (
<div>
{Object.keys(mocks).map((mockKey) => (
<div key={mockKey}>
<label htmlFor={mockKey}>Mock {mockKey}</label>
<input {...getInputProps(mockKey)} />
</div>
))}
</div>
);
}
Dynamic API Mocking
This part has a little more to unpack.
// DevTools.tsx
export const mockServer = setupWorker();
function DevTools() {
const [mocks, setMocks] = React.useState({});
const mockServerReady = React.useRef(false);
const activeMocks = React.useMemo(
() =>
Object.entries(mocks)
.filter(([, shouldMock]) => shouldMock)
.flatMap(([key]) => mocks[key]),
[mocks]
);
React.useEffect(() => {
mockServer.start().then(() => {
mockServerReady.current = true;
});
return () => {
mockServer.resetHandlers();
mockServer.stop();
};
}, []);
React.useEffect(() => {
if (mockServerReady.current) {
flushMockServerHandlers();
}
}, [state.mock]);
function flushMockServerHandlers() {
mockServer.resetHandlers();
addHandlersToMockServer(activeMocks);
}
function addHandlersToMockServer(handlers) {
mockServer.use(...handlers);
}
}
First, we create a ref to track whether the mock server is ready.
function DevTools() {
const mockServerReady = React.useRef(false);
}
Then we create a list of all the active mocks to pass into MSW.
function DevTools() {
const mockServerReady = React.useRef(false);
const activeMocks = React.useMemo(
() =>
Object.entries(mocks)
.filter(([, shouldMock]) => shouldMock)
.flatMap(([key]) => mocks[key]),
[mocks]
);
}
When the dev tools initialize, we want to start the server, and set the mockServerReady ref to true. When it unmounts, we reset all the handlers and stop the server.
function DevTools() {
const mockServerReady = React.useRef(false);
const activeMocks = React.useMemo(
() =>
Object.entries(mocks)
.filter(([, shouldMock]) => shouldMock)
.flatMap(([key]) => mocks[key]),
[mocks]
);
React.useEffect(() => {
mockServer.start().then(() => {
mockServerReady.current = true;
});
return () => {
mockServer.resetHandlers();
mockServer.stop();
};
}, []);
}
Finally, whenever we check a checkbox, we reset all the mocks and add whichever handlers are checked within mocks.
function DevTools() {
const mockServerReady = React.useRef(false);
const activeMocks = React.useMemo(
() =>
Object.entries(mocks)
.filter(([, shouldMock]) => shouldMock)
.flatMap(([key]) => mocks[key]),
[mocks]
);
React.useEffect(() => {
mockServer.start().then(() => {
mockServerReady.current = true;
});
return () => {
mockServer.resetHandlers();
mockServer.stop();
};
}, []);
React.useEffect(() => {
if (mockServerReady.current) {
flushMockServerHandlers();
}
}, [state.mock]);
}
That's all folks!
Top comments (1)
I'm a bit confused seeing the setState and state.mock in your examples, shouldn't it be serMocks and mock?
Great content otherwise! Will try it out in a project soon :)