0.9.2 • Published 11 months ago

msw-integration-layer v0.9.2

Weekly downloads
-
License
ISC
Repository
github
Last release
11 months ago

msw-integration-layer

MSW integration layer for usage with @web/dev-server, @web/test-runner and @web/dev-server-storybook.

Defining mocks

feature-a/demo/mocks.js:

import { rest } from 'msw-integration-layer/rest.js';
import mocksFromAnotherFeature from 'another-feature/demo/mocks.js';
 
/**
 * Define mock scenarios
 */
export default {
  /**
   * Return an object from the handler
   */
  default: [
    rest.get('/api/foo', (context) => Response.json({foo: 'bar'}))
  ],
  /**
   * Return native `Response` object from the handler
   */
  error: [
    rest.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
     */
    rest.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
     */
    rest.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
     */
    rest.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: [
    rest.get('/api/foo', async (context) => Response.json({foo: 'bar'})),
    rest.get('/api/foo', async (context) => new Response(JSON.stringify({foo: 'bar'}), {status: 200})),
    rest.get('/api/foo', (context) => Response.json({foo: 'bar'})),
    rest.get('/api/foo', (context) => new Response(JSON.stringify({foo: 'bar'}), {status: 200})),
  ],
  importedMocks: [
    mocksFromAnotherFeature.default,
    rest.get('/api/foo', () => Response.json({foo: 'bar'}))
  ]
}

Context

The context object that gets passed to the handler includes:

rest.get('/api/foo', ({request, cookies, params}) => {
  return Response.json({foo: 'bar'});
});
  • request the native Request object
  • cookies an object based on the request cookies
  • params an object based on the request params

@web/dev-server/@web/dev-server-storybook

feature-a/web-dev-server.config.mjs:

import { storybookPlugin } from '@web/dev-server-storybook';
import { mockPlugin } from 'msw-integration-layer/node.js';

export default {
  nodeResolve: true,
  plugins: [
    mockPlugin(),
    storybookPlugin({ type: 'web-components' }),
  ],
};

You can also add the mswRollupPlugin to your .storybook/main.cjs config for when you're bundling your Storybook to deploy somewhere; your mocks will be deployed along with your Storybook, and will work in whatever environment you deploy them to.

feature-a/.storybook/main.cjs:

module.exports = {
  stories: ['../stories/**/*.stories.{js,md,mdx}'],
  rollupConfig: async config => {
    const { mswRollupPlugin } = await import('msw-integration-layer/node.js');
    config.plugins.push(mswRollupPlugin());
    return config;
  },
};

feature-a/stories/default.stories.js:

import { html } from 'lit';
import { rest } from 'msw-integration-layer/rest.js';
import mocks from '../demo/mocks.js';

export const Default = () => html`<feature-a></feature-a>`;
Default.story = {
  parameters: {
    mocks: mocks.default,
    // or
    mocks: [
      mocks.default,
      otherMocks.error,
      rest.get('/api/bar', () => Response.json({bar: 'bar'})),
      rest.post('/api/baz', () => new Response('', {status: 400})),
    ],
    // or
    mocks: [
      rest.get('/api/users/:id', ({params}) => {
        if (params.id === '123') {
          return Response.json({name: 'frank'});
        }
        return Response.json({name: 'bob'});
      })
    ],
  },
};

@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 'msw-integration-layer/node.js';

export default {
  nodeResolve: true,
  files: ['test/**/*.test.js'],
  plugins: [
    mockPlugin(),
  ],
};

feature-a/test/my-test.test.js:

import { registerMockRoutes, rest } from 'msw-integration-layer';
import mocks from '../demo/mocks.js';
import featureBmocks from 'feature-b/demo/mocks.js';
 
describe('feature-a', () => {
  it('works', async () => {
    registerMockRoutes(rest.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
      rest.get('/api/baz', (context) => Response.json({baz: 'baz'}))
    )
  });
});

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-a uses msw@1.0.0
  • feature-b uses msw@2.0.0
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 MSW@1.0.0
      featureBmocks.default // ❌ uses MSW@2.0.0, incompatible mocks -> MSW@2.0.0 may expect a different service worker, or different API!
    ]
  }
}

msw@2.0.0 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 { rest } from 'msw-integration-layer/rest.js';

rest.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.

0.9.2

11 months ago

0.9.1

11 months ago

0.9.0

11 months ago