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 :)