5.3 Ajax 测试与调试


5.3 Ajax 测试与调试

本节摘要:请求代码的 bug 藏在"浏览器、网络、服务端、脚本"四层里的任何一层,排障能力等于把这四层逐一照亮的能力。本节前半讲调试:网络面板每个列段的读法、慢请求的定位路径、"请求发了但回调没进"这类经典悬案的排查树;后半讲测试:Mock Server 与请求拦截两种隔离手段、用测试替身验证错误分支、以及并发与竞态这类难以复现的问题怎么测。

排障第一站:把网络面板读成一本书

开发者工具的网络面板是 Ajax 开发者的显微镜,但很多人只看状态码是不是绿的。把它读透,需要逐列理解每段的含义:

名称与状态。名称列看请求的 URL 与方法(可按方法、状态码过滤——找失败请求先点状态码筛选 4xx、5xx)。状态列除了数字还有两类特殊标记:失败的红色(网络层没完成,重点查断网、跨域、被扩展拦截)与灰色的已取消或预检(OPTIONS 预检请求会单独成行,跨域问题的第一线索)。

发起程序。哪个脚本发出的请求。追"这个奇怪请求是谁发的"时点这一列,直接跳到调用栈的发起行——埋点上报乱飞、重复请求溯源,全靠它。

大小与时间。大小列区分"传输大小"与"资源大小"——传输 2KB 解压后 50KB,说明压缩在工作;两者接近且很大,先查压缩配置(4.1 节)。时间列点开是分段瀑布:排队(浏览器并发上限或优先级)、连接与 TLS(跨域冷连接的几百毫秒)、首字节等待(服务端慢)、内容下载(体积问题)。分段读法直接指向四个优化方向,4.1 节的病症对应表在这里落地。

标头与预览。标头页签核对请求头(Content-Type 与体匹配吗、Authorization 带了吗、Origin 是什么)与响应头(缓存头、CORS 声明、Content-Disposition)。预览页签看解析后的响应体结构,比源代码页签适合核对 JSON 字段名。

节流与重放。网络限速 presets 模拟弱网(4.1 节的弱网测试);右键请求可复制为 fetch 代码——把一个"能复现的请求"原样搬到脚本里重放,也是给后端报 bug 时附上"可重放证据"的正规做法。

二、经典悬案的排查树

**悬案一:请求发了,回调没进。**2.1 节的开场案例,这里给完整的排查树。第一步看网络面板:请求存在吗?红色失败(查 onerror 是否漏挂——只挂 onload 的代码,网络失败时回调自然一次都不进)还是 200 绿色(继续)。第二步看状态码:200 之外的状态,检查代码是不是只处理了 200(范围判断的遗漏)。第三步看代码结构:事件是不是在 send 之后才挂(缓存命中时错过事件)、是不是同一个 XHR 实例被第二次 open 作废、Fetch 是不是忘了 await 或没返回。四步下来,九成的"回调没进"在此归案。

悬案二:跨域红字。先读控制台的报错文案——它其实指路明确:提到预检或 OPTIONS 未通过,查服务端的允许方法与头部声明(4.2 节);提到响应头缺失,查 Access-Control-Allow-Origin 是否覆盖了页面的 Origin(面板标头页签里能看到实际值,注意端口也要一致);提到凭证,检查是否开了 credentials 但服务端回了通配符。跨域排障的铁律是三件套对表:页面的 Origin、请求带的头、服务端回的声明,逐项比对。

**悬案三:本地好好的,线上不行。**环境差异清单过一遍:接口地址(环境的 baseURL 配置)、HTTPS 混合内容(HTTPS 页面调 HTTP 接口被浏览器拦截,控制台有提示)、网关改写(有些网关会剥自定义头或改写响应——用"复制为 fetch"在控制台重放对比)、缓存(线上 CDN 缓存了旧接口响应,2.2 节的现场二)。

**悬案四:偶发,复现不了。**与竞态和时间相关。三板斧:在关键回调里打时间戳与序号日志(3.4 节的请求序号直接打印);用限速模拟制造慢网络让竞态显形;上报系统里翻当时的错误样本(3.5 节的原始响应上报在这里救命)。

三、测试的一半:隔离网络

请求代码的自动化测试第一原则:不依赖真实网络。真实接口慢(测试套件跑十分钟)、不稳定(网络抖动即用例红)、不可控(想测 500 就得等服务端真挂)。隔离手段两大流派:

Mock Server:起一个本地假服务,按约定路径返回构造数据。优点是与生产链路最接近(真的走了网络层)、语言无关(前端测试与联调都能用)。缺点是要维护一份"假服务"的启动与数据脚本。

请求拦截:在测试环境中把 fetch 或 XHR 替换为可控的假实现(测试工具的内建能力或拦截库)。优点是零启动成本、能精确控制时序("让请求 B 比请求 A 先返回"这种场景,Mock Server 反而难做)。缺点是没走真实网络层(请求构造本身的 bug 测不到)。实践里单测用拦截、联调用 Mock Server,各取所长。

拦截风格的单测示意(伪代码表达结构):

// 测试目标:loadPage 应并行取三个接口 主内容失败时降级 mockFetch({ '/api/order': { status: 200, body: { code: 0, data: orderFixture } }, '/api/buyer': { status: 500 }, // 制造失败 '/api/logi': { status: 200, body: { code: 0, data: logisticsFixture } } }); const page = await loadPage(orderId); expect(page.order).toEqual(orderFixture); // 主内容在 expect(page.logistics).toEqual(logisticsFixture); // 不依赖的部分不受牵连 expect(page.notice).toBe('hidden'); // 失败部分按设计降级

测试的重点排序值得明确:错误分支比成功分支更值得测(成功路径人人手测,失败路径没人手测);边界数据(空数组、null、超长字符串、缺字段——3.5 节数据错误的四种样本)比正常数据更值得测;时序行为(竞态、取消、超时)最值得测也最容易被漏掉。错误分支的覆盖率,是请求层测试的真实成色。

四、难以复现的部分:时序与竞态的测法

竞态测试的关键是剥夺网络的不可控性——拦截层让你手动决定每个假请求何时"到达":

// 用可控的延迟控制器 让后发的请求先返回 const orderA = deferred(); // 手动兑现的 Promise 包装 const orderB = deferred(); mockFetch('/api/list', { delay: orderA.promise, body: { items: ['旧'] } }); mockFetch('/api/list', { delay: orderB.promise, body: { items: ['新'] } }); const p1 = applyFilter('旧条件'); const p2 = applyFilter('新条件'); orderB.resolve(); // 新请求先回 orderA.resolve(); // 旧请求后到 await Promise.all([p1, p2]); expect(renderedItems).toEqual(['新']); // 断言旧结果没有覆盖新状态

取消与超时测试同理:断言"取消后回调不再执行"(给假请求挂一个间谍函数,abort 后兑现它,断言间谍未被调用)、断言"超时后抛 TimeoutError 而非 AbortError"(4.4 节的语义区分)。

并发上限测试:记录假请求的"同时在途数"峰值,断言不超过设定值(3.6 节限流池的验收标准)。

这类测试的共通技巧是把时间从被测系统手里拿走——可控延迟、假时钟(测试工具的虚拟计时器把 setTimeout 快进),让毫秒级的时间游戏变成确定性的步骤。

五、调试心态与流程清单

把前面的内容收拢成一张排障流程卡(遇到请求问题按序走):

  1. 打开网络面板,找到目标请求——不存在则查触发逻辑与条件(事件绑了吗、条件分支进了吗)。
  2. 看状态:红色查网络层(断网、跨域、拦截器);非 2xx 按 3.5 节分类分流。
  3. 看分段耗时:定位慢在哪一段,对应 4.1 节的四个方向。
  4. 核对头与体:请求头声明与体格式匹配、响应头里的缓存与跨域声明。
  5. 回到代码:发起程序列跳到调用栈,核对 2.1 节的生命周期契约。
  6. 仍无头绪:复制为 fetch 在控制台重放、最小化复现、二分注释。

心态上有一条经验之谈:请求层的 bug 几乎从不出在"你以为的逻辑"里,而藏在四层边界上(浏览器机制、网络环境、服务端行为、脚本契约)。排障的顺序应该是先照亮边界(面板与日志),再读自己的代码——顺序反了,就是对着正确的代码 debug 一晚上。

常见疑问解答

断点为什么经常打不到请求回调里

回调由事件循环调度,断点时机与到达时机交错;且压缩后的代码断点会漂移。更顺手的是条件日志断点(不打断执行、只输出变量),或先在源代码面板格式化再下断点。

Mock 的数据和真实接口长得不一样怎么办

用真实响应做样本库:联调时在网络面板把典型响应(成功、各错误分支)存下来作为 fixture 来源,Mock 数据永远以真实结构为模板——契约变了 fixture 跟着变,测试才不撒谎。

上报系统和本地复现哪个优先

都不可少但顺序有讲究:先上报(用户已经在替你"跑测试"了,浪费样本是罪过),再按样本复现。本地无法复现时,上报里带的上下文(3.5 节的类别、状态码、原始响应)就是断案证据。

本节要点回顾

  • 网络面板逐段读:状态(红与灰各有含义)、发起程序(溯源)、时间分段(四类病症对应四个方向)、大小(传输对资源看压缩)。
  • 四大悬案排查树:回调没进(四步走)、跨域(三件套对表)、本地好线上坏(环境差异清单)、偶发竞态(时间戳日志加限速显形)。
  • 测试先隔离网络:单测用拦截(可控时序是独门优势)、联调用 Mock Server;错误分支、边界数据、时序行为是重点排序。
  • 时序测试把时间拿走:可控延迟测竞态、间谍函数测取消、峰值记录测限流——确定性是可测性的前提。
  • 排障先照亮边界再读代码:四层边界(浏览器、网络、服务端、契约)是 bug 的真实藏身处。

建立个人的排障手册

本章的方法可以沉淀成一份个人排障手册,它的形态是一个不断生长的文档,按"症状"组织而不是按"知识点"组织。初始条目直接从本章抄:症状"回调没进"挂四步排查树、"跨域红字"挂三件套对表、"本地好线上坏"挂环境差异清单、"偶发复现不了"挂三板斧。之后每次真实排障,无论成败,追加一条:症状是什么、第一直觉猜了什么、实际根因在哪、哪一步找到了证据。手册的价值在第二列与第四列的落差——你的直觉猜错的地方,就是你的心智模型有洞的地方。

运行半年后手册会呈现出个人化的形状:有人发现自己总把服务端问题当前端逻辑查(那就给"先看网络面板"设成强制第一步),有人总忘记缓存这个变量(那就把"加时间戳重试一次"设为标准动作),有人对竞态类问题嗅觉差(那就把 3.4 节的序号日志做成常备片段)。这种自我诊断的精度,任何通用教程都给不了。团队层面同理:把每人手册里高频的条目合并成团队排障手册,新人入职先读手册再接需求,等于把最贵的学费(线上事故)摊销给了后来者。

最后一条心态上的提醒:排障是加分项不是还债。很多工程师把排查问题的时间视为"拖了开发进度的意外",匆匆绕过(重启、重试、回滚)就算结案。绕过能救急,但同样的故障会在下个迭代换件衣服回来。每次认真走完排查树、找到根因、写进手册的那一小时,回报率远高于多写一小时的新功能——你不仅修了这一件,还预修了未来的一批。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U