Mock Service Worker

使用 Mock Service Worker 在 Storybook 中模擬 API 請求。

在 Github 上檢視

功能

  • 直接在您的 Story 中模擬 Rest 和 GraphQL 請求。
  • 記錄元件在各種情境下的行為。
  • 免費使用其他附加元件取得 a11y、快照和視覺測試。

完整文件和線上演示

安裝和設定

安裝 MSW 和附加元件

使用 npm

npm i msw msw-storybook-addon -D

或使用 yarn

yarn add msw msw-storybook-addon -D

在您的 public 資料夾中為 MSW 產生 service worker。

如果您已在專案中使用 MSW,您可能之前已經執行過此操作,因此您可以跳過此步驟。

npx msw init public/

如果您未使用 public,請參閱 MSW 官方指南,以取得特定框架的路徑。

設定附加元件

透過初始化 MSW 並在 ./storybook/preview.js 中提供 MSW 加載器,在 Storybook 中啟用 MSW。

import { initialize, mswLoader } from 'msw-storybook-addon'

// Initialize MSW
initialize()

const preview = {
  parameters: {
    // your other code...
  },
  // Provide the MSW addon loader globally
  loaders: [mswLoader],
}

export default preview

啟動 Storybook

執行 Storybook 時,您必須將 public 資料夾作為資源提供給 Storybook,以便包含 MSW,否則它將無法在瀏覽器中使用。

這表示您應該在 Storybook 主要設定檔中設定 staticDirs 欄位。如果需要,請參閱文件

npm run storybook

用法

您可以將請求處理程序 (https://mswjs.io/docs/concepts/request-handler) 傳遞到 msw 參數的 handlers 屬性。這通常是處理程序的陣列。

import { http, HttpResponse } from 'msw'

export const SuccessBehavior = {
  parameters: {
    msw: {
      handlers: [
        http.get('/user', () => {
          return HttpResponse.json({
            firstName: 'Neil',
            lastName: 'Maverick',
          })
        }),
      ],
    },
  },
}

進階用法

組合請求處理程序

handlers 屬性也可以是一個物件,其中鍵可以是處理程序的陣列或處理程序本身。這使您能夠使用 參數繼承,從 preview.js 繼承(並選擇性地覆寫/停用)處理程序。

type MswParameter = {
  handlers: RequestHandler[] | Record<string, RequestHandler | RequestHandler[]>
}

假設您有一個應用程式,其中幾乎每個元件都需要以相同方式模擬對 /login/logout 的請求。您可以在 preview.js 中為這些請求設定全域 MSW 處理程序,並將它們捆綁到一個名為 auth 的屬性中,例如

//preview.ts
import { http, HttpResponse } from 'msw'

// These handlers will be applied in every story
export const parameters = {
  msw: {
    handlers: {
      auth: [
        http.get('/login', () => {
          return HttpResponse.json({
            success: true,
          })
        }),
        http.get('/logout', () => {
          return HttpResponse.json({
            success: true,
          })
        }),
      ],
    },
  },
}

然後,您可以在個別的 Story 中使用其他處理程序。Storybook 將合併全域處理程序和 Story 處理程序

import { http, HttpResponse } from 'msw'

// This story will include the auth handlers from .storybook/preview.ts and profile handlers
export const SuccessBehavior = {
  parameters: {
    msw: {
      handlers: {
        profile: http.get('/profile', () => {
          return HttpResponse.json({
            firstName: 'Neil',
            lastName: 'Maverick',
          })
        }),
      },
    },
  },
}

現在假設您想要覆寫 auth 的全域處理程序。您只需在您的 Story 中再次設定它們,這些值將優先採用

import { http, HttpResponse } from 'msw'

// This story will overwrite the auth handlers from preview.ts
export const FailureBehavior = {
  parameters: {
    msw: {
      handlers: {
        auth: http.get('/login', () => {
          return HttpResponse.json(null, { status: 403 })
        }),
      },
    },
  },
}

如果您想停用全域處理程序怎麼辦?您只需將它們設定為 null,它們就會在您的 Story 中被忽略

import { http, HttpResponse } from 'msw'

// This story will disable the auth handlers from preview.ts
export const NoAuthBehavior = {
  parameters: {
    msw: {
      handlers: {
        auth: null,
        others: [
          http.get('/numbers', () => {
            return HttpResponse.json([1, 2, 3])
          }),
          http.get('/strings', () => {
            return HttpResponse.json(['a', 'b', 'c'])
          }),
        ],
      },
    },
  },
}

設定 MSW

msw-storybook-addon 使用預設設定啟動 MSW。 initialize 接受兩個參數

  • options:當在瀏覽器中時,此參數會傳遞到 worker.start(),或者當在 Node 中時,會傳遞到 server.listen(),因此預期相同的類型。
  • initialHandlers:一個 RequestHandler[] 類型,此陣列會展開到瀏覽器中的 setupWorker() 或 Node 中的 setupServer()

一個常見的範例是設定 onUnhandledRequest 行為,因為如果存在未處理的請求,MSW 會記錄警告。

如果您希望 MSW 繞過未處理的請求而不執行任何操作

// .storybook/preview.ts
import { initialize } from 'msw-storybook-addon'

initialize({
  onUnhandledRequest: 'bypass',
})

如果您希望在 Story 發出應該處理但未處理的請求時警告有用的訊息

// .storybook/preview.ts
import { initialize } from 'msw-storybook-addon'

initialize({
  onUnhandledRequest: ({ url, method }) => {
    const pathname = new URL(url).pathname
    if (pathname.startsWith('/my-specific-api-path')) {
      console.error(`Unhandled ${method} request to ${url}.

        This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.

        If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
      `)
    }
  },
})

雖然 組合處理程序 是可行的,但它依賴於 Storybook 的合併邏輯,**只有在您的 Story 參數中的處理程序是物件而不是陣列時才有效**。為了解決這個限制,您可以將初始請求處理程序作為第二個參數直接傳遞給 initialize 函式。

// .storybook/preview.ts
import { http, HttpResponse } from 'msw'
import { initialize } from 'msw-storybook-addon'

initialize({}, [
  http.get('/numbers', () => {
    return HttpResponse.json([1, 2, 3])
  }),
  http.get('/strings', () => {
    return HttpResponse.json(['a', 'b', 'c'])
  }),
])

在 Node.js 中使用附加元件與可移植的 Story

如果您使用的是 可移植的 Story,您需要確保正確應用 MSW 加載器。

Storybook 8.2 或更高版本

如果您正確地 設定專案註釋,透過呼叫 Story 的 play 函式,將會自動應用 MSW 加載器

import { composeStories } from '@storybook/react'
import * as stories from './MyComponent.stories'

const { Success } = composeStories(stories)

test('<Success />', async() => {
  // The MSW loaders are applied automatically via the play function
  await Success.play()
})

Storybook < 8.2

您可以在渲染您的 Story 之前呼叫 applyRequestHandlers 協助程式來執行此操作

import { applyRequestHandlers } from 'msw-storybook-addon'
import { composeStories } from '@storybook/react'
import * as stories from './MyComponent.stories'

const { Success } = composeStories(stories)

test('<Success />', async() => {
  // 👇 Crucial step, so that the MSW loaders are applied
  await applyRequestHandlers(Success.parameters.msw)
  render(<Success />)
})

注意:applyRequestHandlers 公用程式應該是一個內部細節,它由可移植的 Story 自動呼叫,但是由於在 Storybook 7 中不可能,因此由附加元件匯出。它將在即將發布的版本中移除,因此建議您盡可能升級到 Storybook 8。

疑難排解

MSW 干擾 HMR (熱模組替換)

如果您在主控台中遇到諸如 [MSW] Failed to mock a "GET" request to "https://127.0.0.1:6006/4cb31fa2eee22cf5b32f.hot-update.json" 之類的問題,則可能是 MSW 干擾了 HMR。這並不常見,而且似乎只發生在 Webpack 專案中,但是如果發生在您身上,您可以按照此問題中的步驟來修復它

https://github.com/mswjs/msw-storybook-addon/issues/36#issuecomment-1496150729