调试运行手册

调试 MSW 的常见问题。

下面你可以找到开发者在将 Mock Service Worker 集成到他们的应用中时遇到的最常见问题。在 GitHub 上打开问题之前,请先通读此页面,因为下面很有可能有你的问题的答案。

¥Below you can find the most common issues that developers experience when integrating Mock Service Worker into their applications. Please read through this page before opening an issue on GitHub as there’s a decent chance there is an answer to your issue below.

开始之前

¥Before you begin

检查 Node.js 版本

¥Check Node.js version

检查项目使用的 Node.js 版本:

¥Check the version of Node.js your project is using:

node -v

如果低于 Node.js v18,则为 升级到最新的 Node.js 版本。我们不会调查在 Node.js 不受支持的版本上发生的问题。

¥If it’s lower than Node.js v18, upgrade to the latest Node.js version. We do not look into issues happening on unsupported versions of Node.js.

检查 MSW 版本

¥Check MSW version

首先,检查你安装了哪个版本的 msw 包:

¥First, check what version of the msw package you have installed:

npm ls msw

然后,检查最新的发布版本:

¥Then, check the latest publish version:

npm view msw version

如果这两者不同,请在你的项目中升级 msw 版本并查看问题是否仍然存在。

¥If these two differ, upgrade the msw version in your project and see if the issue persists.

调试运行手册

¥Debugging runbook

你已检查环境和 MSW 版本,但问题仍然存在。是时候进行一些调试了。下面,你可以找到在使用 MSW 时遇到任何意外行为时要遵循的分步调试运行手册。

¥You’ve checked the environment and MSW versions but the issue still persists. It’s time to do some debugging. Below, you can find a step-by-step debugging runbook to follow when experiencing any unexpected behavior with MSW.

步骤 1:验证设置

¥Step 1: Verify setup

首先,验证 MSW 是否正确设置。获取 worker/server 实例并向其添加新的 request:start 生命周期事件监听器。

¥First, verify that MSW is correctly set up. Take the worker/server instance and add a new request:start life-cycle event listener to it.

server.events.on('request:start', ({ request }) => {
  console.log('Outgoing:', request.method, request.url)
})

你可以了解有关 生命周期事件 API 的更多信息。

¥You can learn more about the Life-cycle events API.

使用此监听器后,你应该会看到 MSW 拦截的每个传出请求上的控制台消息。控制台消息应如下所示:

¥With this listener in place, you should see the console message on every outgoing request that MSW intercepts. The console message should look like this:

Outgoing: GET https://api.example.com/some/request
Outgoing: POST http://localhost/post/abc-123

你必须查看请求的方法和绝对请求 URL。如果请求方法不同,请调整你的请求处理程序以反映它。如果你看到相对请求 URL,则你的请求客户端或测试环境配置不正确。你必须配置请求客户端的基本 URL 选项以生成绝对请求 URL,并配置测试环境以设置 document.baseURI

¥You must see the request’s method and the absolute request URL. If the request method is different, adjust your request handler to reflect it. If you see a relative request URL, your request client or your testing environment are not configured correctly. You must configure the base URL option of your request client to produce absolute request URLs, and your test environment to have document.baseURI set.

否则,请验证是否打印了有问题的请求。如果是,请继续执行下一步。

¥Otherwise, verify that the problematic request is printed. If it is, continue to the next step.

如果有问题的请求是你的应用发出的唯一请求,请尝试在 MSW 设置后的任何位置添加虚拟获取调用以确认此步骤。例如:

¥If the problematic request is the only request your application makes, try adding a dummy fetch call anywhere after MSW setup to confirm this step. For example:

fetch('https://example.com')

如果没有针对有问题的请求(或任何请求)打印任何消息,则 MSW 可能未正确设置并且无法拦截请求。请参阅 集成说明 并确保你正在按照那里的说明设置库。

¥If there’s no message printed for the problematic request (or any requests), MSW is likely not being set up correctly and cannot intercept the requests. Please refer to the Integration instructions and make sure you are setting up the library as illustrated there.

步骤 2:验证处理程序

¥Step 2: Verify handler

转到你为有问题的请求创建的请求处理程序,并在其解析器函数中添加控制台语句。

¥Go to the request handler you’ve created for the problematic request and add a console statement in its resolver function.

// src/mocks/handlers.js
import { http } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    // The rest of the response resolver here.
  }),
]

当有问题的请求发生在页面/测试上时,你应该会看到此控制台消息。如果这样做,请继续下一步。

¥You should see this console message when the problematic request happens on the page/tests. If you do, continue to the next step.

如果没有消息,MSW 能够拦截请求但无法将其与此处理程序匹配。这可能意味着你的请求处理程序的谓词与实际请求 URL 不匹配。验证谓词是正确的。一些常见问题包括:

¥If there’s no message, MSW is able to intercept the request but cannot match it against this handler. This likely means your request handler’s predicate doesn’t match the actual request URL. Verify that the predicate is correct. Some of the common issues include:

  • 在路径中使用未在 tests/CI 中设置的环境变量(例如 http.get(BASE_URL + '/path'))。检查请求路径的任何动态段并确保它们具有预期值;

    ¥Using an environment variable in the path, which is not set in tests/CI (e.g. http.get(BASE_URL + '/path')). Inspect any dynamic segments of the request path and make sure they have expected values;

  • 请求路径中的拼写错误。仔细检查此运行手册上一步中打印的请求,并查找其中的任何拼写错误/错误。

    ¥Typos in the request path. Carefully examine the request printed in the previous step of this runbook and find any typos/mistakes in it.

如果不确定,请阅读有关使用 MSW 拦截请求的文档:

¥If unsure, please read through the documentation on intercepting requests with MSW:

Intercepting requests

Learn about request interception and how to capture REST and GraphQL requests.

步骤 3:验证响应

¥Step 3: Verify response

如果调用了请求处理程序但请求仍然没有获得模拟响应,下一个要检查的地方是模拟响应本身。在请求处理程序中,跳转到你定义的模拟响应。

¥If the request handler is invoked but the request still doesn’t get the mocked response, the next place to check is the mocked response itself. In the request handler, jump to the mock response(s) you define.

// src/mocks/handlers.js
import { http, HttpResponse } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    return HttpResponse.json({ mocked: true })
  }),
]

验证你是否正在构建有效的响应。你可以将响应分配给变量并将其打印出来进行检查。你还可以提前返回虚拟模拟响应,并查看你的应用是否收到了它。

¥Verify that you are constructing a valid response. You can assign the response to a variable and print it out to inspect. You can also do an early return of a dummy mocked response and see if it’s received by your application.

如果不确定,请阅读有关使用 MSW 模拟响应的信息:

¥If unsure, please read about mocking responses with MSW:

Mocking responses

Learn about response resolvers and the different ways to respond to a request.

如果你没有发现模拟响应的任何问题,请继续下一步。

¥If you haven’t uncovered any issues with the mocked response, proceed to the next step.

步骤 4:验证应用

¥Step 4: Verify application

如果前面的步骤都没有结果,则问题很可能出在你的应用的请求/响应处理逻辑中。转到执行请求和处理响应的源代码,并验证它们是否正确。请仔细遵循请求框架的指导方针,以确保你按预期执行请求。

¥If none of the previous steps have proven fruitful, it’s likely the issue is in the request/response handling logic of your application. Go to the source code that performs the request and handles the response, and verify they are correct. Please follow the guidelines of your request framework carefully to make sure you are performing requests as intended.

如果问题仍然存在,在 GitHub 上打开新问题 并提供最小复制存储库。没有可以可靠地重现问题的重现存储库的问题将自动关闭。

¥If the problem persists, open a new issue on GitHub and provide a minimal reproduction repository. Issues without the reproduction repository where the problem can be reliably reproduced will be automatically closed.

常见问题

¥Common issues

参考错误:fetch 未定义

¥ReferenceError: fetch is not defined

此错误表示当前进程中未定义全局 fetch 函数。这可能有两个原因:

¥This error indicates that the global fetch function is not defined in the current process. This may happen for two reasons:

  1. 你正在使用旧版本的 Node.js (< v17);

    ¥You are using an older version of Node.js (< v17);

  2. 你的环境抑制了该全局函数。

    ¥Your environment suppresses that global function.

升级 Node.js

¥Upgrading Node.js

首先,通过在收到 fetch 错误的同一终端中运行此命令来检查你正在使用的 Node.js 版本:

¥First, check what version of Node.js you are using by running this command in the same terminal you received the fetch error:

node -v

较新的 Node.js 版本附带全局 Fetch API,其中包括全局 fetch 函数。

¥The newer Node.js versions ship with the global Fetch API, which includes the global fetch function.

修复环境

¥Fixing environment

某些工具(如 Jest)会干扰你的 Node.js 环境,强制删除现有的全局变量。如果你正在使用此类工具,请确保将这些全局变量重新添加到其配置中。

¥Some tools, like Jest, meddle with your Node.js environment, forcefully removing present globals. If you are using such tools, make sure you add those globals back in their configurations.

以下是如何配置 Jest 以使用 Node.js 中的全局 Fetch API 的示例。

¥Here’s an example of how to configure Jest to work with the global Fetch API in Node.js.

此问题是由于你的环境由于某种原因没有 Node.js 全局变量而导致的。这通常发生在 Jest 中,因为它故意剥夺了你的 Node.js 全局变量,并且无法完全重新添加它们。因此,你必须自己明确添加它们。

¥This issue is caused by your environment not having the Node.js globals for one reason or another. This commonly happens in Jest because it intentionally robs you of Node.js globals and fails to re-add them in their entirely. As the result, you have to explicitly add them yourself.

在你的 jest.config.js 旁边创建一个 jest.polyfills.js 文件,内容如下:

¥Create a jest.polyfills.js file next to your jest.config.js with the following content:

// jest.polyfills.js
/**
 
 * @note The block below contains polyfills for Node.js globals
 
 * required for Jest to function when running JSDOM tests.
 
 * These HAVE to be require's and HAVE to be in this exact
 
 * order, since "undici" depends on the "TextEncoder" global API.
 
 *  * Consider migrating to a more modern test runner if
 
 * you don't want to deal with this.
 */
 
const { TextDecoder, TextEncoder } = require('node:util')
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
})
 
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
})

确保安装 undici。它是 Node.js 中的官方获取实现。

¥Make sure to install undici. It’s the official fetch implementation in Node.js.

然后,将 jest.config.js 中的 setupFiles 选项设置为指向新创建的 jest.polyfills.js:

¥Then, set the setupFiles option in jest.config.js to point to the newly created jest.polyfills.js:

// jest.config.js
module.exports = {
  setupFiles: ['./jest.polyfills.js'],
}

如果你发现此设置很麻烦,请考虑迁移到现代测试框架,例如 Vitest,它没有任何 Node.js 全局变量问题并提供开箱即用的原生 ESM 支持。

¥If you find this setup cumbersome, consider migrating to a modern testing framework, like Vitest, which has none of the Node.js globals issues and provides native ESM support out of the box.


模拟响应未到达测试

¥Mock responses don’t arrive at tests

HTTP 请求具有异步性质。在测试依赖于这些请求的解析的代码时,例如在收到响应后渲染的 UI 元素,你需要考虑这种异步性。这通常意味着使用测试框架的正确工具来正确等待 UI 元素。

¥HTTP requests have asynchronous nature. When testing code that depends on the resolution of those requests, like a UI element that renders once the response is received, you need to account for that asynchronicity. This often means using the right tools of your testing framework to properly await UI elements.

// test/suite.test.ts
import { render, screen } from '@testing-library/react'
import { Welcome } from '../components/Welcome'
 
it('renders the welcome text', async () => {
  render(<Welcome />)
 
  // Make sure to use "findBy*" methods that will attempt
  // to look up an element multiple times before throwing.
  // You can also use "waitFor" as an alternative.
  await screen.findByText('Hello, Jack')
})

不要引入任意的 setTimeout/sleep 函数,因为它们会使你的测试受到竞争条件的影响!等待异步代码的唯一可靠方法是等待从它派生的状态(即某个 UI 元素已出现在 DOM 中)。

¥Do not introduce arbitrary setTimeout/sleep functions because they subject your tests to race conditions! The only reliable way to await asynchronous code is to await the state that derives from it (i.e. that a certain UI element has appeared in the DOM).


接收过时的模拟响应

¥Receiving stale mock responses

现代请求库(如 SWR、React Query 或 Apollo)通常会引入缓存以保证出色的用户体验和最佳的运行时性能。请注意,测试时不会自动禁用缓存,这可能会导致你的测试在不同的测试套件中接收旧的/错误的数据。

¥Modern request libraries, like SWR, React Query, or Apollo, often introduce cache to guarantee great user experience and optimal runtime performance. Note that caching is not automatically disabled while testing, which may lead to your tests receiving stale/wrong data across different test suites.

请参阅你的请求库的文档,了解如何在测试中正确禁用缓存。

¥Please refer to your request library’s documentation on how to correctly disable cache in tests.

例如,以下是如何使用 SWR 禁用缓存:

¥For example, here’s how to disable cache using SWR:

// test/suite.test.ts
import { cache } from 'swr'
 
beforeEach(() => {
  // Reset the cache before each new test so there are
  // no stale responses when requesting same endpoints.
  cache.clear()
})

使用 jest.useFakeTimers 时请求未解析

¥Requests are not resolving when using jest.useFakeTimers

在 Jest 中使用假计时器时,所有计时器 API 都会被模拟,包括 queueMicrotaskqueueMicrotask API 由 Node.js 中的全局 fetch 在内部使用,以解析请求/响应主体。因此,当使用默认配置的 jest.useFakeTimers() 时,await request.text()await request.json() 等正文读取方法将无法正确解析。

¥When using fake timers in Jest, all timer APIs are mocked, including queueMicrotask. The queueMicrotask API is used internally by the global fetch in Node.js to parse request/response bodies. Because of this, when using jest.useFakeTimers() with the default configuration, body reading methods like await request.text() and await request.json() will not resolve properly.

例如,以下是如何在使用 jest.useFakeTimers() 时防止 Jest 伪造 queueMicrotask 调用:

¥For example, here’s how to prevent Jest from faking the queueMicrotask calls when using jest.useFakeTimers():

jest.useFakeTimers({
  // Explicitly tell Jest not to affect the "queueMicrotask" calls.
  doNotFake: ['queueMicrotask'],
})

请参阅 Jest 的 关于假计时器的文档

¥Please refer to Jest’s documentation on fake timers.


RTK 查询请求未被拦截

¥RTK Query requests are not intercepted

使用 RTK Query 时的一个常见错误是未在 baseQuery 配置中设置 baseUrl。如果没有这个,请求将具有相对 URL,这在 Node.js 中是无操作(有关更多详细信息,请参阅 此问题)。

¥A common mistake when using RTK Query is not setting the baseUrl in the baseQuery configuration. Without this, the requests will have relative URLs, which is a no-op in Node.js (see this issue for more details).

createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: new URL('/your/api/endpoint', location.origin).href,
  }),
})