模拟外部服务
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 码),让错误处理也被覆盖到。正是这一点,才让测试能对正确结果断言——该到的邮件到了、该发的分析事件发了——而不只是断言"没崩"。