@web/mocks
v2.0.0
Published
MSW integration for @web tooling
Downloads
264
Readme
@web/mocks
MSW integration layer for usage with @web/dev-server, @web/test-runner.
Defining mocks
feature-a/demo/mocks.js:
import { http } from '@web/mocks/http.js';
import mocksFromAnotherFeature from 'another-feature/demo/mocks.js';
/**
* Define mock scenarios
*/
export default {
/**
* Return an object from the handler
*/
default: [http.get('/api/foo', context => Response.json({ foo: 'bar' }))],
/**
* Return native `Response` object from the handler
*/
error: [http.get('/api/foo', context => new Response('', { status: 400 }))],
/**
* Handle additional custom logic in the handler, based on url, searchparams, whatever
*/
custom: [
/**
* Customize based on searchParams
*/
http.get('/api/users', ({ request }) => {
const searchParams = new URL(request.url).searchParams;
if (searchParams.get('user') === '123') {
return Response.json({ id: '123', name: 'frank' });
}
return Response.json({ id: '456', name: 'bob' });
}),
/**
* Customize based on params
*/
http.get('/api/users/:id', ({ params }) => {
if (params.id === '123') {
return new Response('', { status: 400 });
}
return Response.json({ id: '456', name: 'bob' });
}),
/**
* Customize based on cookies
*/
http.get('/api/abtest', ({ cookies }) => {
return Response.json({ abtest: cookies.segment === 'business' });
}),
],
/**
* Provide an async fn, a fn returning an object, a fn returning a Response, or just an object
*/
returnValues: [
http.get('/api/foo', async context => Response.json({ foo: 'bar' })),
http.get(
'/api/foo',
async context => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 }),
),
http.get('/api/foo', context => Response.json({ foo: 'bar' })),
http.get('/api/foo', context => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 })),
],
importedMocks: [
mocksFromAnotherFeature.default,
http.get('/api/foo', () => Response.json({ foo: 'bar' })),
],
};Context
The context object that gets passed to the handler includes:
http.get('/api/foo', ({ request, cookies, params }) => {
return Response.json({ foo: 'bar' });
});requestthe nativeRequestobjectcookiesan object based on the request cookiesparamsan object based on the request params
@web/test-runner
The registerMockRoutes function will ensure the service worker is installed, and the mockPlugin takes care of resolving the service worker file, so users don't have to keep this one-time generated service worker file in their own project roots.
feature-a/web-test-runner.config.mjs:
import { mockPlugin } from '@web/mocks/plugins.js';
export default {
nodeResolve: true,
files: ['test/**/*.test.js'],
plugins: [mockPlugin()],
};feature-a/test/my-test.test.js:
import { registerMockRoutes } from '@web/mocks/browser.js';
import { http } from '@web/mocks/http.js';
import mocks from '../demo/mocks.js';
import featureBmocks from 'feature-b/demo/mocks.js';
describe('feature-a', () => {
it('works', async () => {
registerMockRoutes(http.get('/api/foo', () => Response.json({ foo: 'foo' })));
const response = await fetch('/api/foo').then(r => r.json());
expect(response.foo).to.equal('foo');
});
it('works', () => {
registerMockRoutes(
// Current project's mocks
mocks.default, // is an array, arrays get flattened in the integration layer
// Third party project's mocks, that uses a different version of MSW internally
featureBmocks.default,
// Additional mocks
http.get('/api/baz', context => Response.json({ baz: 'baz' })),
);
});
});Mocking requests in node.js
You can also mock requests in node.js:
import { registerMockRoutes } from '@web/mocks/node.js';
import { http } from '@web/mocks/http.js';
registerMockRoutes(
http.get('/api/foo', () => new Response(JSON.stringify({ foo: 'bar' }), { status: 200 })),
);
const r = await fetch('/api/foo').then(r => r.json());
console.assert(r.foo === 'bar');Storybook integration
We created a Storybook addon @web/storybook-addon-mocks.
See it's documentation for more details.
Rationale
Why not use MSW directly?
Large applications may have many features, that themself may depend on other features internally. Consider the following example:
feature-a uses feature-b internally. feature-a wants to reuse the mocks of feature-b, but the versions of msw are different.
feature-auses[email protected]feature-buses[email protected]
import mocks from '../demo/mocks.js';
import featureBmocks from 'feature-b/demo/mocks.js';
const Default = () => html`<feature-a></feature-a>`; // uses `feature-b` internally
Default.story = {
parameters: {
mocks: [
mocks.default, // uses [email protected]
featureBmocks.default, // ❌ uses [email protected], incompatible mocks -> [email protected] may expect a different service worker, or different API!
],
},
};[email protected] may have a different API or it's service worker may expect a different message, event, or data format. In order to ensure forward compatibility, we expose a "middleman" function:
import { http } from '@web/mocks/http.js';
http.get('/api/foo', () => Response.json({ foo: 'bar' }));The middleware function simply returns an object that looks like:
{
method: 'get',
endpoint: '/api/foo',
handler: () => Response.json({foo: 'bar'})
}This way we can support multiple versions of msw inside of our integration layer by acting as a bridge of sorts; the function people define mocks with doesn't directly depend on msw itself, it just creates an object with the information we need to pass on to msw.
That way, feature-a's project controls the dependency on msw (via the msw integration layer package), while still being able to use mocks from other projects that may use a different version of msw themself internally.
In the wrapper, we standardize on native Request and Response objects; the handler function receives a Request object, and returns a Response object. For utility, we also pass cookies and params, since those are often used to conditionally return mocks. This means that the wrapper function only depends on standard, browser-native JS, and itself has no other dependencies, which is a good foundation to ensure forward compatibility.
Requests/Responses
The Request and Response objects used are standard JS Request and Response objects. You can read more about them on MDN.
