external-service-simulation

模拟外部服务

YouTeacher 的端到端测试用真实浏览器跑真实应用,但应用依赖一堆活在它之外的服务——Google 和 LinkedIn 的 OAuth 登录、Google Analytics 4 遥测、reCAPTCHA 人机校验、Mailgun 事务邮件、Sender.net 订阅、以及若干图片 CDN。这些在测试里都不能真打:慢、要花钱、要测试台不该持有的凭据,而且 OAuth 提供方和 reCAPTCHA 根本拒绝被脚本化。于是整条最外层的边界被换成一个本地替身,每个测试都对着这条边界跑,而不是对着真实互联网。

它由两部分组成。一是独立的 mock 服务器mock-service/server.js)——一个朴素的 Node http 服务,按真实厂商返回的形状作答。二是在 Playwright fixture 里按 context 做的拦截tests/e2e/fixtures.ts),在任何测试开跑之前,把浏览器的出站调用改道到那台服务器。

OAuth:把提供方重定向到本地页

fixture 拦截发往 Google 和 LinkedIn OAuth 主机的请求,用一个重定向把它引到 mock 的 authorize 端点,只转发真正要紧的参数——回调 URI、state,以及测试助手事先挂上的 test_id。mock 随后把应用期望的三段式流程演一遍:authorize 回一个带授权码的回调,应用拿这个码换 access token,userinfo 端点再返回一份 profile。把 test_id 一路穿过每一段的意义在于身份:它让每个并行测试拿到属于自己的唯一账号,这样两个 worker 同一瞬间登录也不会撞车。早期就踩过这个坑——授权码只用毫秒时间戳生成,在多 worker 间重复了,结果一个 worker 的登录解析到了另一个 worker 的邮箱——所以现在身份标识直接烤进授权码本身,而不再从时钟推导。

GA4:改写信标,捕获事件

分析这块更棘手,因为浏览器有两条发送路径——一次 sendBeacon、一次 fetch,都指向 Google 的收集器。fixture 在页面层把这两条都打了补丁,凡是明显发往分析收集器的请求都被改写指向 mock 的 collect 端点,另有一个 route 处理器兜住厂商域名作后备。mock 解析 Measurement Protocol 参数——事件名、页面路径、字符串与数值型自定义参数——并把每个事件留在内存里,这样测试事后就能读回分析本该上报的东西并对它断言。初始的分析脚本被放行不动,只有事件流量被改道。

邮件与 OTP 闭环

mock 接受 Mailgun 的发送端点,按收件人为键存下每封邮件,并暴露一个小小的自定义端点供测试读回某收件人的收件箱。这闭合了一条真实的产品回路:测试注册、应用通过邮件"发出"一次性验证码、助手(tests/e2e/helpers/mailgun.ts)轮询那个 mock 收件箱直到邮件到达,用一组回退正则从邮件正文里抠出六位码,再把它喂回登录流程——跟真人从屏幕上读码再敲进去是一样的路径。这个助手还能把存下的邮件 HTML 渲染成截图作为测试产物。存下的邮件只保留很短一段时间、按一个短的保留窗口清扫,免得内存存储无限膨胀。

reCAPTCHA、Sender 与图片 CDN

reCAPTCHA 两头都做了假:mock 提供一段极小的脚本,把 grecaptcha 全局对象打桩,让 execute() 立即 resolve;它的 verify 端点默认返回成功——同时仍能识别标记性 token,让测试有意去走"无效 token"和"低分"这两条分支。Sender.net 订阅 API 被镜像到够用的程度:能加、能读、能删一个订阅者。图片 CDN(图表/二维码生成、占位图)则被短路成一张 1×1 透明 PNG,这样就没有任何东西卡在图片主机上、拖住页面的 load 事件。

为什么是这个形状

这套设计守着一条硬线:测试端到端地跑真实应用,只替换最外层的第三方边界。 这个替身不是一堆写死的答案——它回放真实的请求/响应形状,持有真实状态(邮件、分析事件、订阅者)供测试检视,甚至模拟失败路径(坏的 captcha token、无效的 OAuth 码),让错误处理也被覆盖到。正是这一点,才让测试能对正确结果断言——该到的邮件到了、该发的分析事件发了——而不只是断言"没崩"。

相关

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →