模拟 fetch()

使用 fetch-mock 将 fetch() 模拟添加到 Storybook.js 中

在 Github 上查看

storybook-addon-fetch-mock

Storybook.js 插件使用 fetch-mock 添加了 fetch() 模拟。

Node.js CI Status

为什么要使用 storybook-addon-fetch-mock?

如果您已经在使用 Storybook.js,您可能有一些组件会调用 API 端点。为了确保您的组件 Storybook 文档不依赖于这些 API 端点的可用性,您需要模拟对这些 API 端点的任何调用。如果您的任何组件更改了端点上的数据,这一点尤其重要。

幸运的是,Storybook 生态系统有很多插件可以使模拟 Fetch API 变得更容易。任何这些插件都允许您拦截组件中的真实 API 调用,并返回您想要的任何模拟数据响应。

Storybook 插件 完整的 Fetch 模拟 模拟函数 模拟对象
模拟服务工作线程插件 ✅ 2
模拟 API 请求插件 ❌ 1
storybook-addon-fetch-mock ✅ 3

1 如果您使用 XMLHttpRequest (XHR),模拟 API 请求插件将非常有用,但我们不建议将其用于 Fetch API 模拟。其功能非常基础,一些 Fetch API 请求无法使用此插件模拟。

2 如果您想模拟正在编写的 Fetch API,使用模拟服务工作线程插件编写模拟解析器函数可能是最简单的方法。

3 如果您想模拟编写的 Fetch API,编写简单的 JavaScript 对象可能是模拟的最简单方法。此项目 storybook-addon-fetch-mock 是 fetch-mock 库(一个自 2015 年以来维护良好的、高度可配置的模拟库)的轻量级包装器。它允许您将模拟编写为简单的 JavaScript 对象、解析器函数或两者的组合。

一个简单的示例

假设一个 UnicornSearch 组件使用 fetch() 调用一个端点来搜索独角兽列表。您可以使用 storybook-addon-fetch-mock 绕过实际的 API 端点并返回模拟响应。在按照下面的“安装”说明操作后,您可以像这样配置 UnicornSearch.stories.js

import UnicornSearch from './UnicornSearch';

export default {
  title: 'Unicorn Search',
  component: UnicornSearch,
};

// We define the story here using CSF 3.0.
export const ShowMeTheUnicorns = {
  args: {
    search: '',
  },
  parameters: {
    fetchMock: {
      // "fetchMock.mocks" is a list of mocked
      // API endpoints.
      mocks: [
        {
          // The "matcher" determines if this
          // mock should respond to the current
          // call to fetch().
          matcher: {
            name: 'searchSuccess',
            url: 'path:/unicorn/list',
            query: {
              search: 'Charlie',
            },
          },
          // If the "matcher" matches the current
          // fetch() call, the fetch response is
          // built using this "response".
          response: {
            status: 200,
            body: {
              count: 1,
              unicorns: [
                {
                  name: 'Charlie',
                  location: 'magical Candy Mountain',
                },
              ],
            },
          },
        },
        {
          matcher: {
            name: 'searchFail',
            url: 'path:/unicorn/list',
          },
          response: {
            status: 200,
            body: {
              count: 0,
              unicorns: [],
            },
          },
        },
      ],
    },
  },
};

如果我们在 Storybook 中打开“Show Me The Unicorns”故事,我们可以填写“search”字段为“Charlie”,并且假设 UnicornSearch 调用 fetch()https://example.com/unicorn/list?search=charlie,我们的 Storybook 插件将比较 parameters.fetchMock.mocks 中的每个模拟,直到找到匹配项,并将返回第一个模拟的响应。

如果我们用不同的值填写“search”字段,我们的 Storybook 插件将返回第二个模拟的响应。

安装

  1. 将插件作为开发依赖项安装

    npm i -D storybook-addon-fetch-mock
    
  2. 通过将插件名称添加到 .storybook/main.js 中的 addons 数组来注册 Storybook 插件

    module.exports = {
      addons: ['storybook-addon-fetch-mock'],
    };
    
  3. 可选地,通过在 .storybook/preview.js 中的 parameters 对象中添加 fetchMock 条目来配置插件。有关详细信息,请参阅下面的“为所有故事配置全局参数”部分。

  4. 将模拟数据添加到您的故事中。有关详细信息,请参阅下面的“配置模拟数据”部分。

配置模拟数据

要拦截对 API 端点的 fetch 调用,请添加一个 parameters.fetchMock.mocks 数组,其中包含一个或多个端点模拟。

参数放在哪里?

如果您将 parameters.fetchMock.mocks 数组放在单个故事的导出中,则模拟将仅应用于该故事

export const MyStory = {
  parameters: {
    fetchMock: {
      mocks: [
        // ...mocks go here
      ],
    },
  },
};

如果您将 parameters.fetchMock.mocks 数组放在 Storybook 文件的 default 导出中,则模拟将应用于该文件中的所有故事。但是,如果需要,您仍然可以按故事覆盖模拟。

export default {
  title: 'Components/Unicorn Search',
  component: UnicornSearch,
  parameters: {
    fetchMock: {
      mocks: [
        // ...mocks go here
      ],
    },
  },
};

您还可以将 parameters.fetchMock.mocks 数组放在 Storybook 的 preview.js 配置文件中,但不推荐这样做。有关更好的替代方案,请参阅下面的“为所有故事配置全局参数”部分。

parameters.fetchMock.mocks 数组

当进行 fetch() 调用时,会将 parameters.fetchMock.mocks 数组中的每个模拟与 fetch() 请求进行比较,直到找到匹配项。

每个模拟都应该是一个包含以下可能键的对象

  • matcher(必需):每个模拟的 matcher 对象具有一项或多项用于匹配的条件。如果 matcher 中包含多个条件,则所有条件都必须匹配才能使用该模拟。
  • response(可选):一旦匹配成功,匹配的模拟的 response 将用于配置 fetch() 响应。
    • 如果模拟未指定 response,则 fetch() 响应将使用 HTTP 200 状态且没有主体数据。
    • 如果 response 是一个对象,则这些值将用于创建 fetch() 响应。
    • 如果 response 是一个函数,则该函数应返回一个对象,其值将用于创建 fetch() 响应。
  • options(可选):配置模拟行为的其他选项。

以下是 matcherresponseoptions 的所有可能键的完整列表

const exampleMock = {
  // Criteria for deciding which requests should match this
  // mock. If multiple criteria are included, all of the
  // criteria must match in order for the mock to be used.
  matcher: {
    // Match only requests where the endpoint "url" is matched
    // using any one of these formats:
    // - "url" - Match an exact url.
    //     e.g. "http://www.site.com/page.html"
    // - "*" - Match any url
    // - "begin:..." - Match a url beginning with a string,
    //     e.g. "begin:http://www.site.com"
    // - "end:..." - Match a url ending with a string
    //     e.g. "end:.jpg"
    // - "path:..." - Match a url which has a given path
    //     e.g. "path:/posts/2018/7/3"
    // - "glob:..." - Match a url using a glob pattern
    //     e.g. "glob:http://*.*"
    // - "express:..." - Match a url that satisfies an express
    //     style path. e.g. "express:/user/:user"
    // - RegExp - Match a url that satisfies a regular
    //     expression. e.g. /(article|post)\/\d+/
    url: 'https://example.com/endpoint/search',

    // If you have multiple mocks that use the same "url",
    // a unique "name" is required.
    name: 'searchSuccess',

    // Match only requests using this HTTP method. Not
    // case-sensitive.
    method: 'POST',

    // Match only requests that have these headers set.
    headers: {
      Authorization: 'Basic 123',
    },

    // Match only requests that send a JSON body with the
    // exact structure and properties as the one provided.
    // See matcher.matchPartialBody below to override this.
    body: {
      unicornName: 'Charlie',
    },

    // Match calls that only partially match the specified
    // matcher.body JSON.
    matchPartialBody: true,

    // Match only requests that have these query parameters
    // set (in any order).
    query: {
      q: 'cute+kittenz',
    },

    // When the express: keyword is used in the "url"
    // matcher, match only requests with these express
    // parameters.
    params: {
      user: 'charlie',
    },

    // Match if the function returns something truthy. The
    // function will be passed the url and options fetch was
    // called with. If fetch was called with a Request
    // instance, it will be passed url and options inferred
    // from the Request instance, with the original Request
    // will be passed as a third argument.
    functionMatcher: (url, options, request) => {
      return !!options.headers.Authorization;
    },

    // Limits the number of times the mock can be matched.
    // If the mock has already been used "repeat" times,
    // the call to fetch() will fall through to be handled
    // by any other mocks.
    repeat: 1,
  },

  // Configures the HTTP response returned by the mock.
  response: {
    // The mock response’s "statusText" is automatically set
    // based on this "status" number. Defaults to 200.
    status: 200,

    // By default, the optional "body" object will be converted
    // into a JSON string. See options.sendAsJson to override.
    body: {
      unicorns: true,
    },

    // Set the mock response’s headers.
    headers: {
      'Content-Type': 'text/html',
    },

    // The url from which the mocked response should claim
    // to originate from (to imitate followed directs).
    // Will also set `redirected: true` on the response.
    redirectUrl: 'https://example.com/search',

    // Force fetch to return a Promise rejected with the
    // value of "throws".
    throws: new TypeError('Failed to fetch'),
  },

  // Alternatively, the `response` can be a function that
  // returns an object with any of the keys above. The
  // function will be passed the url and options fetch was
  // called with. If fetch was called with a Request
  // instance, it will be passed url and options inferred
  // from the Request instance, with the original Request
  // will be passed as a third argument.
  response: (url, options, request) => {
    return {
      status: options.headers.Authorization ? 200 : 403,
    };
  },

  // An object containing further options for configuring
  // mocking behaviour.
  options: {
    // If set, the mocked response is delayed for the
    // specified number of milliseconds.
    delay: 500,

    // By default, the "body" object is converted to a JSON
    // string and the "Content-Type: application/json"
    // header will be set on the mock response. If this
    // option is set to false, the "body" object can be any
    // of the other types that fetch() supports, e.g. Blob,
    // ArrayBuffer, TypedArray, DataView, FormData,
    // URLSearchParams, string or ReadableStream.
    sendAsJson: false,

    // By default, a Content-Length header is set on each
    // mock response. This can be disabled when this option
    // is set to false.
    includeContentLength: false,
  },
};

为所有故事配置全局参数

以下选项旨在用于 Storybook 的 preview.js 配置文件中。

// .storybook/preview.js
export const parameters = {
  fetchMock: {
    // When the story is reloaded (or you navigate to a new
    // story, this addon will be reset and a list of
    // previous mock matches will be sent to the browser’s
    // console if "debug" is true.
    debug: true,

    // Do any additional configuration of fetch-mock, e.g.
    // setting fetchMock.config or calling other fetch-mock
    // API methods. This function is given the fetchMock
    // instance as its only parameter and is called after
    // mocks are added but before catchAllMocks are added.
    useFetchMock: (fetchMock) => {
      fetchMock.config.overwriteRoutes = false;
    },

    // After each story’s `mocks` are added, these catch-all
    // mocks are added.
    catchAllMocks: [
      { matcher: { url: 'path:/endpoint1' }, response: 200 },
      { matcher: { url: 'path:/endpoint2' }, response: 200 },
    ],

    // A simple list of URLs to ensure that calls to
    // `fetch( [url] )` don’t go to the network. The mocked
    // fetch response will use HTTP status 404 to make it
    // easy to determine one of the catchAllURLs was matched.
    // These mocks are added after any catchAllMocks.
    catchAllURLs: [
      // This is equivalent to the mock object:
      // {
      //   matcher: { url: 'begin:http://example.com/' },
      //   response: { status: 404 },
      // }
      'http://example.com/',
    ],
  },
};
作者
  • johnalbin
    johnalbin
支持
    Angular
    Ember
    HTML
    Preact
    React
    React Native
    Svelte
    Vue
    Web Components
标签