ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

undici MockPool 完整指南:按路由拦截 HTTP 请求、定义 Mock 响应与编写无网络依赖的测试

undici MockPool 完整指南:按路由拦截 HTTP 请求、定义 Mock 响应与编写无网络依赖的测试 后端网络通信【免费下载链接】undiciAn 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构造函数与 MockPoolOptionsMockPool继承Pool并实现Interceptable接口使经由它发出的请求能够与已注册的 Mock 匹配。new MockPool(origin[, options])origin{string} 与该 mock pool 关联的 origin只应包含协议、主机名和端口。options{MockPoolOptions} 扩展Pool的选项。返回{MockPool}agent选项是必填的且必须实现Agent接口即暴露dispatch函数否则抛出InvalidArgumentError。对应实现位于 lib/mock/mock-pool.jsconstructor (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 }会抛InvalidArgumentErroragent: new MockAgent()则正常通过并且mockPool instanceof Dispatcher为真确认它实现了完整的 Dispatcher API。参数MockPoolOptions扩展PoolOptions额外字段如下选项类型说明默认值agent{MockAgent}与该 mock pool 关联的 agent必填—ignoreTrailingSlash{boolean}匹配拦截请求的 path 时是否忽略末尾斜杠falseignoreTrailingSlash在构造时被存入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。默认GETbody{string|RegExp|Function}HTTP 请求体的匹配器。函数形式签名为(body: string) booleanheaders{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} 被拦截请求的 originbody{string|null} 被拦截请求的正文参数MockResponseOptions字段类型说明headers{Object} 附加到 Mock 响应的头trailers{Object} 附加到 Mock 响应的尾控制匹配次数MockScopeMockScope与单个MockInterceptor关联用于配置已定义回复的使用次数方法说明返回delay(waitInMs)延迟关联回复waitInMs毫秒后再返回{MockScope}persist()让关联回复无限期匹配每个匹配请求都收到该响应{MockScope}times(repeatTimes)让关联回复只匹配固定次数会被persist()覆盖{MockScope}实现上lib/mock/mock-interceptor.jsdelay()要求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?hellothereseeya, method: POST, body: form1data1form2data2, 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?hellothereseeya, { method: POST, body: form1data1form2data2, 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 formdata, headers: { User-Agent: undici, Host: /^example\.com$/ } }).reply(200, foo) const { statusCode, body } await request(http://localhost:3000/foo, { method: GET, body: formdata, 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-lengthimport { 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, indefinitelyimport { 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?foobar) // Matches and returns the mock注意当path使用RegExp或函数时匹配的是包含按字母序排列的查询参数的路径lib/mock/mock-utils.js 的safeUrl()会对查询参数sort()。上面的函数示例因此对?foobar这类单参数场景很稳健多参数场景需要自行处理参数顺序。生命周期管理close()/dispatch()/request()/cleanMocks()mockPool.close()返回{Promise}pool 关闭后以undefined兑现。关闭 mock pool优雅等待已入队的请求完成并从关联的MockAgent中移除。源码 lib/mock/mock-pool.js 显示其实现先等待原始Pool.close()完成再将连接数置 0并从agent[kClients]中删除该 originasync [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 的关键在于它并非凭空拦截而是对标准派发流程的接管。完整链路如下构造时替换 dispatchMockPool在构造函数里保存原始this.dispatchkOriginalDispatch随后this.dispatch buildMockDispatch.call(this)lib/mock/mock-pool.js、lib/mock/mock-utils.js。按 origin 路由请求派发时MockAgent.dispatch()先通过this.get(opts.origin)定位到对应的 MockPool必要时新建再把派发委托给内部 Agentlib/mock/mock-agent.js。mockDispatch 逐条件过滤进入 Mock 匹配后lib/mock/mock-utils.js 的mockDispatch()依据注册的kDispatches列表按 path → method → body → headers 的顺序逐层过滤先剔除已消费consumed的条目并按路径匹配再匹配方法、正文、请求头命中失败时依次抛出带具体原因路径/方法/正文/头的MockNotMatchedError。回复交付命中后通过handler.onRequestStart→onResponseStart→onResponseData→onResponseEnd模拟一次完整响应生命周期响应头与尾由generateKeyValues()编码为 key-value 数组若配置了delay则用setTimeout推迟handleReply若请求在延迟期间被 abort则通过controller.abort()触发onResponseError。消费与清理回复完成后deleteMockDispatch()从列表中移除该 dispatchpending状态则用于MockAgent.assertNoPendingInterceptors()的断言防止测试结束后还有未消费的 Mock。测试与断言实践仓库的 test/mock-pool.js 是 MockPool 行为的最完整佐证覆盖了构造校验非法agent抛InvalidArgumentErrorMockPool实现 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:testconst { 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[]1param[]2、param1,2,3这类非标准语法可在new MockAgent({ acceptNonStandardSearchParameters: true })中开启。生命周期收尾测试结束后调用mockAgent.close()或在MockAgent上使用assertNoPendingInterceptors()确保没有遗留的未消费 Mock。掌握 MockPool 的这组 API 后你可以完全脱离真实服务器为 HTTP 客户端代码编写确定性的单元测试配合setGlobalDispatcher或局部dispatcher注入两种接入方式覆盖正常响应、延迟、错误、重试等各类场景。赞分享后端网络通信【免费下载链接】undiciAn HTTP/1.1 client, written from scratch for Node.js项目地址https://gitcode.com/gh_mirrors/un/undici点击查看免费下载相关推荐undici MockAgent 实战指南用拦截器与 Mock 响应为 Node.js HTTP 代码编写可靠测试undici MockAgent 实战指南用拦截器与 Mock 响应为 Node.js HTTP 代码编写可靠测试 MockAgent 是 undici 提供后端网络通信Hutool请求拦截HTTP请求响应拦截Hutool请求拦截HTTP请求响应拦截 还在为HTTP请求的全局处理而烦恼每次都要手动添加相同的header、记录日志、处理异常Hutool的HTTP拦后端开发工具Selenium网络拦截请求Mock与响应修改全攻略Selenium网络拦截请求Mock与响应修改全攻略 引言为什么需要网络拦截 在Web应用测试过程中你是否遇到过以下痛点 依赖第三方API导致测试不稳测试开发工具上一篇Stillcolor深度解析如何让Mac屏幕不再闪烁彻底告别眼睛疲劳下一篇Stillcolor3分钟彻底解决Mac屏幕闪烁问题告别眼睛疲劳的终极方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表