boundary()

将网络拦截范围限定在给定的边界内。

调用签名

¥Call signature

server.boundary() 函数接受 callback 函数并返回一个新函数,该函数具有与给定 callback 相同的调用签名,但已绑定。

¥The server.boundary() function accepts a callback function and returns a new function with the same call signature as the given callback but bound.

function boundary<Callback extends (...args: Array<unknown>) => unknown>(
  callback: Callback
): (...args: Parameters<Callback>) => ReturnType<Callback>

用法

¥Usage

server.boundary() API 旨在提供网络行为隔离。在边界内对请求拦截所做的任何修改都只会影响该边界,不会影响其他任何内容。

¥The server.boundary() API is designed to provide the network behavior isolation. Any modifications to the request interception made within a boundary will only affect that boundary and nothing else.

import { HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer()
server.listen()
 
function app() {
  fetch('https://example.com')
 
  server.boundary(() => {
    // This runtime handler override will only affect
    // the network within this server boundary.
    server.use(
      http.get('https://example.com', () => {
        return HttpResponse.error()
      })
    )
 
    fetch('https://example.com')
  })()
}

在上面的例子中,第一个 fetch() 调用将由最初提供给 setupServer() 调用的请求处理程序处理,在本例中为无。但是,边界内的相同 fetch() 调用将收到网络错误(HttpResponse.error()),因为在边界内添加了相应的请求处理程序覆盖。

¥In the example above, the first fetch() call will be handled by whichever request handlers initially provided to the setupServer() call, which in this case is none. The same fetch() call within the boundary, however, will receive a network error (HttpResponse.error()) because the respective request handler override was added within the boundary.

由于边界提供了范围隔离,因此你无需重置请求处理程序。但是,如果你希望重置该服务器边界内的网络行为,则需要重置它们。

¥Since the boundary provides scope isolation, you don’t need to reset the request handlers. You do need to reset them, however, if you wish to reset the network behavior within that server boundary.

server.boundary() API 利用 AsyncLocalStorage,这意味着当前范围和边界子范围中的所有网络都将受到请求处理程序覆盖的影响。

¥The server.boundary() API utilizes AsyncLocalStorage, which means that all the network in the current scope and child scopes of the boundary will be affected by request handler overrides.

服务器边界接受传递给回调函数的任何参数并返回该函数返回的任何内容。考虑到这一点,即使在回调预期返回某些内容的情况下,你也可以使用它。

¥The server boundary accepts whichever arguments passed to the callback function and returns whatever that function returns. With that in mind, you can use it even in situations when the callback is expected to return something.

// Let's imagine you are using a server boundary
// in a backend framework that expects route handlers
// to return Fetch API Response instances.
router.get(
  '/resource',
  // This boundary will accept whatever arguments
  // were given to it by "router.get", and return a
  // new Response instance to the router.
  server.boundary((request) => {
    return new Response('Hello world')
  })
)

独立

¥Standalone

此 API 可以独立用于各种目的,例如范围网络自省、调试和开发。在下面的例子中,server.boundary() 用于自省 Express 中作为 POST /resource 路由处理的一部分发生的所有网络请求。

¥This API can be used standalone for various purposes, like a scoped network introspection, debugging, and development. In the example below, the server.boundary() is used to introspect all the network requests that are happening as a part of the POST /resource route handling in Express.

import express from 'express'
import { http } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer()
server.listen()
 
const app = express()
 
app.post('/resource', (req, res) => {
  server.boundary(() => {
    server.use(
      http.all('*', ({ request }) => {
        console.log(request.method, request.url)
      })
    )
 
    handleRequest(req, res)
  })()
})

并发测试运行

¥Concurrent test runs

server.boundary() API 主要用于支持现代测试框架中的并发测试运行。由于请求处理程序的总列表保存在 setupServer() 范围的内存中,因此这会引入全局状态问题。如果多个并发测试调用 server.use(),则这些请求处理程序覆盖最终会影响并行运行的无关测试。

¥The server.boundary() API is primarily designed to support concurrent test runs in modern test frameworks. Since the total list of request handlers is kept in-memory in the setupServer() scope, this introduces a global state problem. If multiple concurrent tests call server.use(), those request handler override will end up affecting irrelevant tests that run in parallel.

在每个测试中引入服务器边界可以解决此问题,并防止请求处理程序覆盖影响不相关的测试。看看 Vitest 中此并发测试套件中 server.boundary() 的实际使用情况:

¥Introducing a server boundary in each test solves this problem and prevents request handler overrides from ever affecting irrelevant tests. Take a look at how server.boundary() is used in practice in this concurrent test suite in Vitest:

import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
 
const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
beforeAll(() => {
  server.listen()
})
 
afterAll(() => {
  server.close()
})
 
it.concurrent(
  'fetches the user',
  server.boundary(async () => {
    // This test doesn't introduce any request handlers override.
    // The network within this test will be resolved against the
    // initial request handlers provided to "setupServer()" call.
    const response = await fetch('https://example.com/user')
    const user = await response.json()
    expect(user).toEqual({ name: 'John' })
  })
)
 
it.concurrent(
  'handles the server error',
  server.boundary(async () => {
    // This test makes the user requests return a 500 response.
    // Fetching the user in this scope, and any nested scopes,
    // will always result in a 500 response.
    server.use(
      http.get('https://example.com/user', () => {
        return new HttpResponse(null, { status: 500 })
      })
    )
    const response = await fetch('https://example.com/user')
    expect(response.status).toBe(500)
  })
)
 
it.concurrent(
  'handles network errors',
  server.boundary(async () => {
    // This test makes the user requests fail with a network error.
    server.use(
      http.get('https://example.com/user', () => {
        return HttpResponse.error()
      })
    )
 
    await expect(fetch('https://example.com/user')).rejects.toThrow(
      'Failed to fetch'
    )
  })
)

尽管每个测试用例都依赖于特定的网络状态,但使用 server.boundary() API 可以确定该状态的范围和 “freeze”,从而在并发测试运行中产生可预测的网络行为。

¥Although each test case relies on a particular network state, using the server.boundary() API allows to scope and “freeze” that state, resulting in predictable network behavior in concurrent test runs.

嵌套边界

¥Nested boundaries

每当创建服务器边界时,它都会将来自更高范围的任何现有请求处理程序视为初始请求处理程序。

¥Whenever a server boundary is created, it treats whichever existing request handlers from the higher scope as the initial request handlers.

const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
server.boundary(async () => {
  // The user request will return a 200 JSON response
  // as described in the initial request handlers
  // provided to the "setupServer" call above.
  await fetch('https://example.com/user')
})()

边界内的任何请求处理程序覆盖都会添加到请求处理程序的初始列表中,类似于它们在常规 .use() 使用中的方式。

¥Any request handler overrides within the boundary are prepended to the initial list of request handlers, similar to how they are in the regular .use() usage.

当服务器边界嵌套在另一个服务器边界内时,上边界所具有的任何请求处理程序状态都被视为嵌套边界的初始状态。

¥When a server boundary is nested within another server boundary, whichever request handler state the upper boundary has is treated as the initial state for the nested boundary.

const server = setupServer(
  http.get('https://example.com/user', () => {
    return HttpResponse.json({ name: 'John' })
  })
)
 
// This server boundary has the following request handlers:
// - (initial) GET /user -> 200 OK
// - (override) POST /login -> 500 Internal Server Error
server.boundary(() => {
  server.use(
    http.post('https://example.com/login', () => {
      return new HttpResponse(null, { status: 500 })
    })
  )
 
  // This server boundary has the following request handlers:
  // - (initial) GET /user -> 200 OK
  // - (initial) POST /login -> 500 Internal Server Error
  // - (override) DELETE /post -> 404 Not Found
  server.boundary(() => {
    server.use(
      http.delete('https://example.com/post', () => {
        return new HttpResponse(null, { status: 404 })
      })
    )
 
    // Resetting the request handlers will remove any
    // request handler overrides added in *this* boundary.
    // The resulting request handlers will be:
    // - (initial) GET /user -> 200 OK
    // - (initial) POST /login -> 500 Internal Server Error
    server.resetHandlers()
  })()
})()