☰
undici MockPool 完整指南:按路由拦截 HTTP 请求、定义 Mock 响应与编写无网络依赖的测试
2026/9/27 9:33:45 网站建设 项目流程
  • 后端
  • 网络
  • 通信

【免费下载链接】undici

An HTTP/1.1 client, written from scratch for Node.js

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载

MockPool是 undici 内置测试利器:它继承自Pool,能够拦截与已注册路由匹配的 HTTP 请求,并直接返回编程预设的 Mock 响应,全程不触碰真实网络。本文将以 docs/docs/api/MockPool.md 为主线,结合 lib/mock/mock-pool.js、lib/mock/mock-interceptor.js、lib/mock/mock-utils.js 等源码与 test/mock-pool.js 测试,系统讲解 MockPool 的构造、intercept()路由匹配规则、MockInterceptor/MockScope响应定义 API、生命周期管理以及底层 dispatch 替换原理,读完即可在项目中落地完整的接口 Mock 测试方案。

MockPool 是什么:与 Pool、MockClient 的关系

MockPool是一个拦截请求的Pool:凡是匹配到已注册路由的请求,都会被它截获并用 Mock 响应回复,而不是连接网络。它由MockAgent创建,并暴露与MockClient完全一致的拦截 API。

在绝大多数场景下,你不应直接new MockPool(),而是通过mockAgent.get(origin)获取。一旦 MockAgent 被注册为全局 dispatcher,任何 origin 与该 pool 匹配的请求都会在 pool 持有的 Mock 中进行匹配。

MockPool与MockClient的选择由 MockAgent 的connections配置决定:从源码 lib/mock/mock-agent.js 的工厂方法可见,当options.connections === 1时返回MockClient,否则返回MockPool(对应一个允许多连接的 Pool 语义):

[kFactory] (origin) { const mockOptions = Object.assign({ agent: this }, this[kOptions]) return this[kOptions] && this[kOptions].connections === 1 ? new MockClient(origin, mockOptions) : new MockPool(origin, mockOptions) }

在 docs/docs/api/MockAgent.md 中,mockAgent.get(origin)的返回值类型同样被标注为{MockClient|MockPool},且同一 origin 的后续调用返回同一实例。

获取 MockPool 的两种模块写法:

import { MockAgent } from 'undici' const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost:3000')
const { MockAgent } = require('undici') const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost:3000')

构造 MockPool:构造函数与 MockPoolOptions

MockPool继承Pool并实现Interceptable接口,使经由它发出的请求能够与已注册的 Mock 匹配。

new MockPool(origin[, options])

  • origin{string} 与该 mock pool 关联的 origin,只应包含协议、主机名和端口。
  • options{MockPoolOptions} 扩展Pool的选项。
  • 返回:{MockPool}

agent选项是必填的,且必须实现Agent接口(即暴露dispatch函数),否则抛出InvalidArgumentError。对应实现位于 lib/mock/mock-pool.js:

constructor (origin, opts) { if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { throw new InvalidArgumentError('Argument opts.agent must implement Agent') } super(origin, opts) ... }

test/mock-pool.js 对构造行为做了明确验证:agent: { get: 'not a function' }会抛InvalidArgumentError;agent: new MockAgent()则正常通过;并且mockPool instanceof Dispatcher为真,确认它实现了完整的 Dispatcher API。

参数:MockPoolOptions

扩展PoolOptions,额外字段如下:

选项类型说明默认值
agent{MockAgent}与该 mock pool 关联的 agent,必填—
ignoreTrailingSlash{boolean}匹配拦截请求的 path 时是否忽略末尾斜杠false

ignoreTrailingSlash在构造时被存入this[kIgnoreTrailingSlash],随后在intercept()时作为默认值注入每个拦截器(见下文)。路径匹配层面,lib/mock/mock-utils.js 中的removeTrailingSlash()会持续剥掉路径末尾的/(空路径则归一为/),配合matchValue实现斜杠无关匹配。

注册拦截路由:mockPool.intercept(options)

  • options{MockPoolInterceptOptions} 被拦截请求的匹配条件。
  • 返回:{MockInterceptor} 用于定义 Mock 响应的拦截器。

intercept()在 mock pool 上注册一条路由,并返回一个MockInterceptor,用于定义匹配请求的回复(如reply()或replyWithError())。其实现会先把 pool 级别的ignoreTrailingSlash与调用方传入的选项合并,再创建拦截器:

intercept (opts) { return new MockInterceptor( opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, this[kDispatches] ) }

每次intercept()调用只被单个匹配请求消费一次。要匹配多个请求,需要按期望请求数各调用一次intercept(),或对返回的MockScope使用persist()/times()。当没有任何已注册 Mock 匹配请求时,会尝试发起真实请求;若 MockAgent 已禁用网络连接,则抛出MockNotMatchedError(错误码UND_MOCK_ERR_MOCK_NOT_MATCHED,定义于 lib/mock/mock-errors.js)。

一个请求只有在所有定义的匹配条件都通过时才会被拦截。匹配器(matcher)的三种形式行为如下:

匹配器类型通过条件
string与值完全相等
RegExp正则表达式匹配
Function函数返回true

该语义在 lib/mock/mock-utils.js 的matchValue()中实现:字符串用===全等比较,正则用match.test(value),函数则要求严格返回true。

参数:MockPoolInterceptOptions

字段类型说明
path{string|RegExp|Function}HTTP 请求路径的匹配器。函数形式签名为(path: string) => boolean。使用RegExp或函数时,匹配的是包含按字母序排列的全部查询参数的请求路径;使用string时,查询参数可通过query提供
method{string|RegExp|Function}HTTP 方法的匹配器。函数形式签名为(method: string) => boolean。默认:'GET'
body{string|RegExp|Function}HTTP 请求体的匹配器。函数形式签名为(body: string) => boolean
headers{Object|Function}HTTP 请求头的匹配器。可以是「头名 → string/RegExp/(value: string) => boolean匹配器」的映射,也可以是接收全部头并返回boolean的单个函数。使用映射时,请求必须匹配每个定义的头;未列出的额外头不影响匹配
query{Object}查询字符串参数的匹配器。仅当path为string时生效
ignoreTrailingSlash{boolean}匹配时是否忽略path末尾斜杠;未设置时继承 mock pool 的ignoreTrailingSlash选项

MockInterceptor构造时的规范化逻辑(lib/mock/mock-interceptor.js)值得一提:

  • path必填,缺失时抛InvalidArgumentError('opts.path must be defined');opts本身必须是对象。
  • method未定义时默认'GET';字符串形式会被toUpperCase()统一大写。
  • path为字符串时,若提供query则用serializePathWithQuery(path, query)拼出完整路径;否则用new URL(opts.path, 'data://')解析出pathname + search,从而剔除 URI 中按 RFC 3986 不应发给服务器的 fragment(对应 undici issue #1245)。

定义响应:MockInterceptorAPI

匹配请求的回复行为通过返回的MockInterceptor定义。默认情况下,reply()和replyWithError()只定义第一个匹配请求的行为,后续请求不受影响,除非对返回的MockScope使用persist()或times()。

reply(statusCode[, data[, responseOptions]])

  • statusCode{number} Mock 响应的状态码。
  • data{string|Buffer|Object|Function} Mock 响应的正文。对象会被序列化为 JSON;字符串和Buffer原样发送。函数形式签名为(opts: MockResponseCallbackOptions) => string | Buffer | Object,以请求为入参计算响应正文。
  • responseOptions{MockResponseOptions} 附加响应选项。默认:{}。
  • 返回:{MockScope}

正文的序列化规则在 lib/mock/mock-utils.js 的getResponseData()中实现:Buffer/Uint8Array/ArrayBuffer等字节容器原样保留;普通对象走JSON.stringify;其余 truthy 值转字符串;空值输出''。

reply(callback)

  • callback{Function} 签名为(opts: MockResponseCallbackOptions) => { statusCode, data, responseOptions }的函数,以请求为入参动态计算全部回复选项(而非仅正文)。回调可以异步:返回的 Promise 会被等待,且必须 resolve 为相同结构。
  • 返回:{MockScope}

在 lib/mock/mock-interceptor.js 中,reply()会先判断参数是否为函数:若是,则把回调包装成延迟求值的 dispatch 数据(wrappedDefaultsCallback),并在内部用isPromise()(来自node:util)区分同步与异步回调;回调返回值必须是对象,否则抛InvalidArgumentError('reply options callback must return an object')。

replyWithError(error)

  • error{Error} 匹配请求时抛出的错误。
  • 返回:{MockScope}

该错误会通过handler.onResponseError(null, error)交付给请求方,并在命中后删除对应 dispatch(见 lib/mock/mock-utils.js 的dispatchMockReply)。

defaultReplyHeaders(headers)/defaultReplyTrailers(trailers)

  • headers{Object} 头名到值的映射。
  • 返回:{MockInterceptor}

设置该拦截器后续每个回复都会携带的默认头/尾,与具体回复上设置的头/尾是叠加关系。实现上存入this[kDefaultHeaders]/this[kDefaultTrailers],在createMockScopeDispatchData()中通过对象展开合并:{ ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }。

replyContentLength()

  • 返回:{MockInterceptor}

为后续每个回复自动计算并设置content-length头。打开开关(this[kContentLength] = true)后,content-length会以响应体字节长度填充,并放在defaultReplyHeaders与responseOptions.headers之间(后者可覆盖)。

回调与响应参数

参数:MockResponseCallbackOptions

传入reply()的 data 回调与 options 回调的实参,携带被拦截请求的信息:

字段类型说明
path{string} 被拦截请求的路径
method{string} 被拦截请求的方法
headers{Object|Headers} 被拦截请求的头
origin{string} 被拦截请求的 origin
body{string|null} 被拦截请求的正文

参数:MockResponseOptions

字段类型说明
headers{Object} 附加到 Mock 响应的头
trailers{Object} 附加到 Mock 响应的尾

控制匹配次数:MockScope

MockScope与单个MockInterceptor关联,用于配置已定义回复的使用次数:

方法说明返回
delay(waitInMs)延迟关联回复waitInMs毫秒后再返回{MockScope}
persist()让关联回复无限期匹配,每个匹配请求都收到该响应{MockScope}
times(repeatTimes)让关联回复只匹配固定次数(会被persist()覆盖){MockScope}

实现上(lib/mock/mock-interceptor.js):delay()要求waitInMs为大于 0 的整数;times()要求repeatTimes为大于 0 的整数,否则抛InvalidArgumentError。消费逻辑位于 lib/mock/mock-utils.js 的mockDispatch():

mockDispatch.timesInvoked++ mockDispatch.consumed = !mockDispatch.persist && timesInvoked >= times mockDispatch.pending = timesInvoked < times

即:被消费完且未persist的 dispatch 标记为consumed,后续请求不再命中;delay则通过setTimeout实现延迟回复。

典型拦截模式示例

以下示例全部来自 docs/docs/api/MockPool.md,可整体复制运行。

基础 Mock 请求

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo' }).reply(200, 'foo') const { statusCode, body } = await request('http://localhost:3000/foo') console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // data foo }

使用 data 回调动态生成正文

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/echo', method: 'GET' }).reply(200, ({ headers }) => ({ message: headers.message })) const { statusCode, body } = await request('http://localhost:3000/echo', { headers: { message: 'hello world!' } }) console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // {"message":"hello world!"} }

使用 options 回调动态计算全部回复参数

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/echo', method: 'GET' }).reply(({ headers }) => ({ statusCode: 200, data: { message: headers.message } })) const { statusCode, body } = await request('http://localhost:3000/echo', { headers: { message: 'hello world!' } }) console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // {"message":"hello world!"} }

使用异步 options 回调

import { readFile } from 'node:fs/promises' import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/fixture', method: 'GET' }).reply(async ({ path }) => ({ statusCode: 200, data: await readFile(new URL('./fixture.json', import.meta.url)) })) const { statusCode, body } = await request('http://localhost:3000/fixture') console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // contents of fixture.json }

多个拦截

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo') mockPool.intercept({ path: '/hello', method: 'GET' }).reply(200, 'hello') const fooResult = await request('http://localhost:3000/foo') for await (const data of fooResult.body) { console.log('data', data.toString('utf8')) // data foo } const helloResult = await request('http://localhost:3000/hello') for await (const data of helloResult.body) { console.log('data', data.toString('utf8')) // data hello }

匹配 query、body、请求头,并带 headers 与 trailers 回复

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo?hello=there&see=ya', method: 'POST', body: 'form1=data1&form2=data2', headers: { 'User-Agent': 'undici', Host: 'example.com' } }).reply(200, { foo: 'bar' }, { headers: { 'content-type': 'application/json' }, trailers: { 'Content-MD5': 'test' } }) const { statusCode, headers, trailers, body } = await request( 'http://localhost:3000/foo?hello=there&see=ya', { method: 'POST', body: 'form1=data1&form2=data2', headers: { foo: 'bar', 'User-Agent': 'undici', Host: 'example.com' } } ) console.log('response received', statusCode) // response received 200 console.log('headers', headers) // { 'content-type': 'application/json' } for await (const data of body) { console.log('data', data.toString('utf8')) // {"foo":"bar"} } console.log('trailers', trailers) // { 'content-md5': 'test' }

注意:请求头里多带了foo: 'bar'仍能匹配成功——这正是「未列出的额外头不影响匹配」的体现。

混合使用不同匹配器形式

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: /^GET$/, body: (value) => value === 'form=data', headers: { 'User-Agent': 'undici', Host: /^example\.com$/ } }).reply(200, 'foo') const { statusCode, body } = await request('http://localhost:3000/foo', { method: 'GET', body: 'form=data', headers: { foo: 'bar', 'User-Agent': 'undici', Host: 'example.com' } }) console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // data foo }

回复错误

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).replyWithError(new Error('kaboom')) try { await request('http://localhost:3000/foo', { method: 'GET' }) } catch (error) { console.error(error.message) // kaboom }

默认回复头 / 默认回复尾 / 自动 content-length

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }) .defaultReplyHeaders({ foo: 'bar' }) .reply(200, 'foo') const { headers } = await request('http://localhost:3000/foo') console.log('headers', headers) // headers { foo: 'bar' }
import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }) .defaultReplyTrailers({ foo: 'bar' }) .reply(200, 'foo') const { trailers } = await request('http://localhost:3000/foo') console.log('trailers', trailers) // trailers { foo: 'bar' }
import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }) .replyContentLength() .reply(200, 'foo') const { headers } = await request('http://localhost:3000/foo') console.log('headers', headers) // headers { 'content-length': '3' }

持久化回复与固定次数回复

import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo').persist() await request('http://localhost:3000/foo') // Matches and returns the mock await request('http://localhost:3000/foo') // Matches again, indefinitely
import { MockAgent, setGlobalDispatcher, request } from 'undici' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo').times(2) await request('http://localhost:3000/foo') // Matches and returns the mock await request('http://localhost:3000/foo') // Matches and returns the mock await request('http://localhost:3000/foo') // No match; a real request is attempted

用函数匹配 path(自定义查询参数解析)

当路径中的查询参数顺序不固定,或需要自定义解析逻辑时,可以完全接管匹配:

import { MockAgent, setGlobalDispatcher, request } from 'undici' import querystring from 'node:querystring' const mockAgent = new MockAgent() setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') const matchPath = (requestPath) => { const [pathname, search] = requestPath.split('?') const requestQuery = querystring.parse(search) if (!pathname.startsWith('/foo')) { return false } return requestQuery.foo === 'bar' } mockPool.intercept({ path: matchPath, method: 'GET' }).reply(200, 'foo') await request('http://localhost:3000/foo?foo=bar') // Matches and returns the mock

注意:当path使用RegExp或函数时,匹配的是包含按字母序排列的查询参数的路径(lib/mock/mock-utils.js 的safeUrl()会对查询参数sort())。上面的函数示例因此对?foo=bar这类单参数场景很稳健;多参数场景需要自行处理参数顺序。

生命周期管理:close()/dispatch()/request()/cleanMocks()

mockPool.close()

  • 返回:{Promise},pool 关闭后以undefined兑现。

关闭 mock pool,优雅等待已入队的请求完成,并从关联的MockAgent中移除。源码 lib/mock/mock-pool.js 显示其实现:先等待原始Pool.close()完成,再将连接数置 0,并从agent[kClients]中删除该 origin:

async [kClose] () { await promisify(this[kOriginalClose])() this[kConnected] = 0 this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) }
import { MockAgent } from 'undici' const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost:3000') await mockPool.close()

mockPool.dispatch(options, handlers)

  • options{DispatchOptions} 请求选项。
  • handlers{DispatchHandler} 请求生命周期中调用的处理器。
  • 返回:{boolean},若 dispatcher 忙碌、调用方应等待后再派发则返回false,否则返回true。

派发请求并让它与已注册 Mock 匹配。这个对dispatcher.dispatch(options, handlers)的覆写,正是request等所有高层方法的 Mock 行为的驱动源头。从源码看,构造时this.dispatch被替换为buildMockDispatch.call(this)返回的闭包;该闭包在agent.isMockActive为真时进入 Mock 匹配流程,匹配失败且网络已禁用时抛MockNotMatchedError,否则回落到原始originalDispatch发起真实请求。

mockPool.request(options[, callback])

  • options{DispatchOptions}
  • callback{Function}(可选)未请求 Promise 时以响应为参调用。
  • 返回:{Promise},未提供callback时以 Mock 响应兑现。

执行请求并让它在已注册 Mock 中解析。继承自Pool,完整参数与返回值文档见dispatcher.request(options[, callback])。

import { MockAgent } from 'undici' const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo') const { statusCode, body } = await mockPool.request({ origin: 'http://localhost:3000', path: '/foo', method: 'GET' }) console.log('response received', statusCode) // response received 200 for await (const data of body) { console.log('data', data.toString('utf8')) // data foo }

mockPool.cleanMocks()

  • 返回:{undefined}

移除 mock pool 上所有已注册的拦截器,此前定义且尚未消费的 Mock 不再匹配任何请求。实现仅一行:this[kDispatches] = []。test/mock-pool.js 中有对应测试,先intercept().reply(...)再cleanMocks(),随后请求即不再被拦截。

import { MockAgent } from 'undici' const mockAgent = new MockAgent() const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo' }).reply(200, 'foo') mockPool.cleanMocks()

底层原理:dispatch 如何被替换与匹配流程

理解 MockPool 的关键在于它并非凭空拦截,而是对标准派发流程的接管。完整链路如下:

  1. 构造时替换 dispatch:MockPool在构造函数里保存原始this.dispatch(kOriginalDispatch),随后this.dispatch = buildMockDispatch.call(this)(lib/mock/mock-pool.js、lib/mock/mock-utils.js)。
  2. 按 origin 路由:请求派发时,MockAgent.dispatch()先通过this.get(opts.origin)定位到对应的 MockPool(必要时新建),再把派发委托给内部 Agent(lib/mock/mock-agent.js)。
  3. mockDispatch 逐条件过滤:进入 Mock 匹配后,lib/mock/mock-utils.js 的mockDispatch()依据注册的kDispatches列表按 path → method → body → headers 的顺序逐层过滤:先剔除已消费(consumed)的条目并按路径匹配,再匹配方法、正文、请求头,命中失败时依次抛出带具体原因(路径/方法/正文/头)的MockNotMatchedError。
  4. 回复交付:命中后通过handler.onRequestStart→onResponseStart→onResponseData→onResponseEnd模拟一次完整响应生命周期;响应头与尾由generateKeyValues()编码为 key-value 数组;若配置了delay则用setTimeout推迟handleReply;若请求在延迟期间被 abort,则通过controller.abort()触发onResponseError。
  5. 消费与清理:回复完成后deleteMockDispatch()从列表中移除该 dispatch;pending状态则用于MockAgent.assertNoPendingInterceptors()的断言(防止测试结束后还有未消费的 Mock)。

测试与断言实践

仓库的 test/mock-pool.js 是 MockPool 行为的最完整佐证,覆盖了:

  • 构造校验:非法agent抛InvalidArgumentError;MockPool实现 Dispatcher 接口。
  • 拦截校验:intercept()缺参抛opts must be an object;缺path抛opts.path must be defined;缺method时默认GET不报错。
  • 全局 dispatcher 用法:setGlobalDispatcher(mockPool)后,request()命中 Mock 而不会打到真实服务器(测试中真实服务器断言should not be called)。
  • 本地 dispatcher 用法:mockPool可作为请求的dispatcher选项局部注入。
  • MockPool.request 路径:通过mockPool.request()直接消费 Mock 响应(含 headers、trailers)。
  • cleanMocks:清理后 Mock 不再匹配。

典型测试骨架(结合 undici 的node:test):

const { test } = require('node:test') const { MockAgent, setGlobalDispatcher, request } = require('undici') test('GET /foo returns mocked 200', async (t) => { const mockAgent = new MockAgent() t.after(() => mockAgent.close()) setGlobalDispatcher(mockAgent) const mockPool = mockAgent.get('http://localhost:3000') mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo') const { statusCode, body } = await request('http://localhost:3000/foo') // assert statusCode === 200, read body ... })

常见问题与最佳实践

  • MockNotMatchedError 何时出现:请求未命中任何 Mock 且MockAgent已disableNetConnect()时抛出(错误码UND_MOCK_ERR_MOCK_NOT_MATCHED)。若网络连接未被禁用,未命中的请求会回落到真实网络请求——测试时务必用disableNetConnect()兜底,避免测试误连外网。
  • 单次消费语义:intercept()默认只命中一次,多请求场景记得用persist()或times(n),否则会出现「Mock 被提前消费导致后续请求走真实网络」的隐性问题。
  • query 匹配注意点:path为RegExp/函数时匹配的是排序后的查询参数路径;为string时可用query字段单独声明参数匹配。
  • 方法与头匹配的健壮性:方法匹配时字符串会被统一为大写;请求头匹配时未列出的额外头不影响结果;MockAgent还支持acceptNonStandardSearchParameters(如param[]=1&param[]=2、param=1,2,3这类非标准语法),可在new MockAgent({ acceptNonStandardSearchParameters: true })中开启。
  • 生命周期收尾:测试结束后调用mockAgent.close()或在MockAgent上使用assertNoPendingInterceptors(),确保没有遗留的未消费 Mock。

掌握 MockPool 的这组 API 后,你可以完全脱离真实服务器为 HTTP 客户端代码编写确定性的单元测试,配合setGlobalDispatcher或局部dispatcher注入两种接入方式,覆盖正常响应、延迟、错误、重试等各类场景。

  • 后端
  • 网络
  • 通信

【免费下载链接】undici

An HTTP/1.1 client, written from scratch for Node.js

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载
上一篇:Stillcolor深度解析:如何让Mac屏幕不再"闪烁",彻底告别眼睛疲劳?
下一篇:Stillcolor:3分钟彻底解决Mac屏幕闪烁问题,告别眼睛疲劳的终极方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询