用GitHub Actions把端到端测试跑成每天100次的自动巡检

📅 发布时间:2026/10/12 7:27:31
用GitHub Actions把端到端测试跑成每天100次的自动巡检
GitHub Actions 跑端到端测试一次两次谁都跑过但我现在这套流水线是每天自动触发 100 次。你没看错100 次。之前团队里所有人听到这个数字的第一反应都是你是不是疯了其实没疯。这个频率背后是我的一个执念端到端测试如果只在 merge 前跑一遍那它只能挡住显而易见的错误拦不住间歇性冒头的问题——而后一种恰恰是线上故障里占比最高的那类。所以我干脆把 GitHub Actions 当作一个免费的、按量计费的定时巡检器用它把 E2E 测试打成一天 100 发的实弹射击。这篇文章就把这套方案从头到尾拆开讲清楚为什么是 100 次、Workflow 怎么配、测试脚本要做什么特殊处理、跑多了之后踩了哪些坑以及最终我得到的真实数据。中间还会穿插一些对端到端这个概念的理解——不只是页面点一点还包括网络链路的验证这部分我会用 iperf 做补充探测。如果你也想搞一套高频自动回归或者正在为 flaky test 头疼这篇应该对你有用。1. 为什么每天要跑 100 次端到端测试1.1 一个低频 Bug 逼出来的方案先说背景。我们维护的是一个面向 B 端客户的 SaaS 产品前端和后端都在快速迭代每周至少发两次版本。之前的质量防线是PR 合并前跑一次 CI里面包含少量端到端用例每天晚上再跑一次完整的 E2E 回归大概 40 分钟。听起来够用对吧但真正让人崩溃的是线上出现过一个持续了三天才被用户反馈的故障——某条支付回调在特定时序下会偶发失败概率大约 3%。Nightly 那 40 分钟里这条用例恰好每次都侥幸通过。那次故障之后我意识到一个道理凡是没有高频执行过的自动化用例本质上都是一次性脚本。它只能证明此刻可用无法证明长时间可用。而端到端测试的价值恰恰在于验证系统在真实链路下的长期稳定性一次两次的成功样本根本不够。于是我开始琢磨能不能把 GitHub Actions 当成一个 7x24 小时的哨兵让它按固定节奏反复跑 E2E 测试跑出统计学意义来。1.2 100 次这个数字是怎么算出来的需要说明每天 100 次并不是拍脑袋。GitHub Actions 的 schedule 事件最小粒度是每 5 分钟一次但我实测下来免费/付费账号在高峰期都不会严格准点触发通常有 2 到 15 分钟的延迟。所以我选择每 15 分钟触发一次一天 24 小时是 96 次再加上手动触发一次备用的完整压测批次补足到 100 左右。另一个考量是并发窗口。100 次如果叠在一起跑测试环境和被测系统都会被压垮跑出来的结果全是没有区分度的凶杀现场。所以我用concurrency做了互斥同一时间只允许一个任务实例在跑。这样平均一天下来每次任务间隔约 10 到 15 分钟既保证高频又留出了合理的缓冲时间。还有第三种模式如果你想换在夜间低峰期集中跑 100 轮专门看系统的稳定性曲线。这两种我都实测过。每 15 分钟一次的方案更适合巡检式回归能覆盖一整天里不同时段的服务端负载变化夜间集中 100 轮则更像容量压测适合发版前突击验证。我这篇主要讲前者但里面很多排查经验对后者也适用。2. 端到端测试在 GitHub Actions 里的整体设计2.1 技术选型为什么是 GitHub Actions 加 Playwright选 GitHub Actions 而不是自建 Jenkins 或 GitLab Runner原因很朴素省心而且按量计费的成本对我们这个团队规模来说几乎可以忽略。GitHub Actions 的 macOS 和 Windows runner 要额外付费Ubuntu 免费额度内包含 2000 分钟/月单个 job 跑 8 分钟的 E2E 任务一天 100 次需要 24000 分钟左右必然超免费额度——但这部分成本换来了一个不用维护的调度平台比在公司里养一台 7x24 小时的物理机划算得多。测试框架选了 Playwright。它和 Cypress 的核心差异在于Playwright 的浏览器上下文隔离是做在协议层跑完一个用例自动关闭对 CI 这种反复执行的环境非常友好。而且 Playwright 的 trace viewer 能直接回放失败用例的完整操作流对高频跑测来说这功能不是锦上添花是保命用的。后者我后面讲坑的时候会具体展开。被测环境用的是独立 staging 环境部署在固定的公网地址上由 nightly 流水线维护更新。E2E 任务每次启动前会先获取当前 staging 的 commit 号测试数据的 title 里也会带上这个信息后续统计失败归属时能快速区分是环境代码问题还是测试脚本问题。2.2 触发策略设计schedule、workflow_dispatch 和防重入机制最开始我只配了 schedule 定时触发但很快发现一个尴尬的问题GitHub Actions 的 schedule 触发在仓库长期没有 push 活动的情况下可能直接不跑。官方文档的解释是定时任务可能因为仓库长时间无活动被跳过但我遇到的场景是明明每周都有 commit任务依然偶发迟到。所以我加了workflow_dispatch作为手动补射通道同时在 workflow 里记录每次触发的时间戳方便追踪调度延迟。防重入也很关键。如果不加concurrency限制前一个任务还没跑完后一个定时触发又来了两批测试同时操作同一份测试数据结果会互相污染产生一批幽灵失败。我的配置是concurrency: group: e2e-regression cancel-in-progress: falsecancel-in-progress: false意思是如果上一个任务还没结束新的触发就排队等待而不是直接掐断旧的。排队机制比取消更安全因为端到端测试的现场信息截图、trace一旦被中断就可能不完整影响失败分析。2.3 测试数据隔离CI 环境里的脏数据陷阱高频跑测对数据隔离的要求跟普通 CI 完全不同。普通 CI 跑一次测试数据脏了就脏了第二天反正会重建。但一天 100 次的频率下如果每个用例都往同一个测试账号里造数用不了半天账号状态就一团乱用例开始出现昨天还能过、今天莫名其妙挂的情况。我的做法是做了一层测试数据塑形所有 E2E 用例的初始化数据都通过 API 直接打到 staging 环境而不是通过 UI 去填表单生成。UI 操作只覆盖业务主流程数据准备全部走接口速度快且可重复。对于无法通过 API 创建的第三方依赖数据则在用例开始前做幂等清理比如按固定前缀删除昨天的历史记录。这套机制跑起来之后由数据污染导致的假失败比例从 30% 降到了 3% 以内。3. 核心实现Workflow 配置和测试脚本的落地细节3.1 Workflow YAML一份可以直接抄的配置下面是我实际跑在仓库里的核心配置注释已经写清了每个节点的用途。你可以直接复制过去改成自己的参数name: e2e-regression on: schedule: - cron: */15 * * * * workflow_dispatch: concurrency: group: e2e-regression cancel-in-progress: false env: BASE_URL: ${{ secrets.STAGING_BASE_URL }} TEST_ACCOUNT: ${{ secrets.E2E_TEST_ACCOUNT }} TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} jobs: e2e: runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: browser: [chromium] steps: - name: Checkout uses: actions/checkoutv4 - name: Setup Node uses: actions/setup-nodev4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run E2E tests run: npx playwright test --browser${{ matrix.browser }} continue-on-error: true - name: Upload Playwright Report if: always() uses: actions/upload-artifactv4 with: name: playwright-report-${{ github.run_number }} path: playwright-report/ retention-days: 7 - name: Parse results and send notification if: always() run: node .github/scripts/parse-results.js几个细节值得单独说timeout-minutes 设置为 20 分钟比测试套件实际耗时 8 分钟留了很大余量。高频跑测最怕的是某个用例因为外部原因挂起把整个队列堵死。设置超时之后单次任务最坏情况也就是 20 分钟能在预定的 15 分钟间隔内错峰重叠不会无限堆积。continue-on-error: true这一行看着很违反直觉——跑的明明是测试失败了你却不让任务失败原因是我想让 Playwright 测试结束后后续的报告解析和通知步骤仍然执行。如果让任务直接失败后面的 steps 会被跳过失败信息就只能埋在日志里没人收到提醒。我在下一步的解析脚本里读取测试结果决定最终用exit(1)让任务失败。逻辑是把执行测试和判定失败分开这样失败信息的可观测性会好很多。upload-artifact 设置 retention-days: 7是因为每天 100 次的频率如果不设保留期一周就能堆积几百上千个报告压缩包把仓库的 Actions 存储空间干爆。我一般只在定位疑难问题时临时改成保留 30 天平时 7 天足够。3.2 测试脚本层为了扛住 100 次执行需要做的改造Playwright 测试脚本本身在低频模式下和普通 E2E 写法没区别但高频模式下必须加上三样东西自动重试、trace 保留、结构化结果输出。自动重试我用的是 Playwright 内置机制在playwright.config.ts里配置retries: 2。这个重试覆盖的逻辑基础是端到端测试的偶发失败有相当一部分来自外部条件波动比如 staging 环境刚好在发版、CDN 缓存还没刷新、第三方服务响应延迟等。一次失败不一定是产品问题但如果三次都挂在同一个断言上那基本就是真故障。重试策略是一道成本很低的过滤网能把误报率压低一个数量级。trace 保留只针对失败用例做配置如下use: { trace: retain-on-failure, screenshot: only-on-failure, video: retain-on-failure, }这样每次失败我都能拿到现场回放。高频跑测有个隐藏陷阱一个用例一天失败 3 次你会想当然觉得它是个 flaky 测试想把它屏蔽掉。但如果有 trace你会发现 3 次失败里其实有 2 次的失败页面完全不同一个是空白页一个是报错弹窗另一个才是断言失败。这说明背后根本不是同一个 bug。屏蔽测试是最容易做、但也最容易掩盖真问题的操作。结构化结果输出是我的一个小技巧在 globalTeardown 里把每个用例的执行时间、状态、错误消息汇总写入一个 JSON 文件然后在 workflow 里用解析脚本读取将摘要写入 GitHub Actions Summary。这样每次跑完PR 页面或 Actions 页面直接能看到这轮跑挂了这个用例、失败断言是啥、耗时多长不用点开层层日志。3.3 端到端工具链顺带解决的问题链路层验证我把 iperf 也拉进来了标题里的热词说到了 TCP/IP 发包收包测试我想借此展开一个常被忽视的维度应用层的端到端测试通过不代表底层网络链路健康。我做 Web E2E 时遇到过好几次测试超时排查到最后发现是 staging 机房到 CI runner 之间的网络抖动TCP 重传率高得离谱和产品代码一点关系都没有。为了避免误判我在 E2E workflow 里加了一个前置网络探测 step用一个轻量的 TCP 连通性检查脚本先测一下 runner 到 staging 关键端口的连通性和往返延迟。如果延迟超过阈值就直接跳过本轮测试并记录为网络异常不进入 Playwright 执行阶段。这个步骤把网络问题从应用问题里剥离开统计失败率时就不会把两种情况混在一起算。如果你想做更彻底的网络层端到端验证iperf 是个趁手的工具。它可以在 Windows 和 Linux 上分别起 server 和 client测 TCP/UDP 的实际吞吐、丢包和抖动。我通常在 Windows 主机上跑iperf3 -s开服务端在 CI runner 上用iperf3 -c host -t 10 -i 1做 10 秒带宽测试看返回的 Jitter 和 Lost 数据。注意 iperf 的测试结果受两端机器性能影响很大第一次测试先要跑一个 baseline后续对比才有意义。这个工具的操作视频网上很多但核心套路就这一套起服务端、跑客户端、读三个指标——带宽、抖动、丢包率。4. 跑了 100 次之后真实数据、报告和成本4.1 通过率曲线揭示的东西这套流水线稳定跑了两周之后我拉了一下数据。每天 100 次每次套件内包含 18 条关键主流程用例两周累计执行 14000 次用例。整体通过率是 97.3%。看着不低但拆开看才有意思失败分布呈现明显的二八形态。18 条用例里有 3 条贡献了 70% 的失败次数而这 3 条恰恰都是涉及时间控件的用例。比如预约任务在截止时间前 15 分钟被正确拦截这条时区处理在 staging 服务器和 runner 上不一致导致边界时间判断出现偏差。这类问题在每天跑一次的场景下可能要一个月才暴露一次但在每天 100 次的频率下第三天就现了原形。还有一条非常重要的曲线把失败次数按小时聚合之后我发现上午 10 点到下午 4 点的失败率是凌晨的 3 倍。一开始怀疑是被测服务白天负载高后来查日志发现是 staging 环境的自动扩缩容策略在白天频繁触发扩缩容瞬间会有几秒延迟尖峰刚好撞上 E2E 的断言超时。这个发现直接推动了后端团队调整了 staging 的扩缩容冷却时间。4.2 报告汇总和通知链路失败不是跑完就结束高频执行的最大挑战不是执行而是每天 100 次跑完人不可能每次都蹲在页面前看结果。所以我把通知链路做成了三级漏斗第一级GitHub Actions Summary。每次运行结束解析脚本把本轮通过率、失败用例列表、平均耗时写进去。这是给后续排查用的原始素材。第二级飞书机器人推送。如果本轮存在失败用例就往测试群里推一条消息附上失败用例名称和 trace 下载链接。白天工作时间我要求实时推送凌晨的失败则汇总到早上 8 点统一推一次不然 3 点被一条用例 x 失败的推送吵醒人也受不住。第三级自动建 Issue。如果同一个用例在 24 小时内失败超过 5 次脚本自动在仓库里创建 bug issue标题带用例名和失败次数正文贴上最近 5 次的失败链接。这个机制让我不用去翻原始数据就能知道哪个问题正在高频发生、严重程度如何。4.3 配额与费用100 次/天到底花了多少算一笔账。单个 E2E job 实际耗时约 8 分钟含依赖安装、浏览器下载、测试执行每天 100 次就是 800 分钟。GitHub Actions 的免费额度是 2000 分钟/月但这是所有 job 共享的我们仓库本身还有 PR CI 等任务加一起一个月大约要消耗 30000 分钟。超出的部分按 Linux runner 的标准费率计费大约是每 1000 分钟 8 美元。一个月跑下来Actions 账单大约在 220 美元上下。这个数字看绝对值不便宜但考虑到它替代了一个需要人盯着的巡检岗位、且只在真实故障前预警这笔钱我们团队内部都觉得花得值。如果你预算紧张可以砍掉 webkit 和 mobile 的矩阵组合只保留 chromium费用能再降一半左右。5. 高频跑测踩过的坑和排查思路5.1 flaky test 治理先别急着屏蔽先做归因这是整个项目里我最想分享的经验。高频跑测几天后第一个冒出来的成功经验就是那个用例老挂把它 ignore 掉吧。我拦住了。不是因为屏蔽本身有问题而是因为屏蔽之前要先删掉一个关键变量——你到底知不知道它为什么挂。我整理了一个失败归因清单失败表现首要排查方向定位方法页面白屏/加载超时CDN、静态资源回源、路由缓存看 trace 里的 network 面板断言在特定按钮上挂元素被遮挡、动画未结束、状态未刷新播放 trace观察点击前 3 秒页面状态接口断言偶发失败后端超时、mock 数据错位、数据库锁对比失败前后两次请求的响应时间时间相关断言挂时区差异、系统时钟偏差、边界条件检查 runner 与 staging 的 UTC 时区配置用这个清单处理了 14 天内的所有失败之后我才敢下结论这条用例是真有 bug还是这是测试稳定性问题。清理完逻辑后我屏蔽掉了一条无意义的用例其余全部通过代码修正或者环境修正恢复了。5.2 Actions 调度延迟schedule 不是硬实时GitHub Actions 的 schedule 触发官方文档明确说了不保证准时实际测试发现短则几秒长则 20 分钟。这对低频跑测完全无所谓但对按 15 分钟间隔设计的高频任务影响很大。比如我规划 6:00 跑一批实际 6:12 才启动跑 8 分钟下一批原定 6:15 触发又可能等到 6:31整个节奏就乱了。后来我不强依赖 cron 的准点性而是在测试脚本里做冷却判断如果距离上次成功执行结束不足 5 分钟本轮直接跳过执行只记录一个 skipped避免任务叠加。同时我把 schedule 的 cron 表达式中闹钟密度从每 15 分钟提高到每 10 分钟一次给调度延迟留出缓冲。实测下来实际有效执行次数仍然稳定在每天 95 到 100 次之间。5.3 runner 系统环境的干扰Windows 上做端到端也要防杀毒我们的被测服务同时发布到 Windows 服务器上所以在 Windows runner 上也跑了一组 E2E 验证。Windows runner 遇到的坑比 Linux 多得多Windows Defender 实时扫描会在冷启动时占满 CPU导致 Playwright 浏览器启动超时还有系统代理设置某些 CI 脚本会顺手设置 HTTP_PROXY 环境变量而如果这个变量指向一个不可达的代理所有 HTTP 请求都会失败。注意这里的代理是业务代码里的网络配置不是说任何工具我只是想提醒 runner 环境变量管理要小心。解决方式一是把C:\Users\runneradmin\.cache\ms-playwright目录加入 Defender 排除列表二是统一在 workflow 里显式删除不需要的代理环境变量。还有一个 Windows 特有的坑路径长度限制。E2E 生成的截图和 trace 文件名太长时Windows runner 会直接报错需要在 playwright.config.ts 里缩短输出文件名的前缀。6. 想抄作业的人配置速查和避坑清单6.1 关键参数速查表配置项推荐值说明触发频率*/15 * * * *按 15 分钟间隔配合跑步逻辑约等于每天 100 次concurrency group按仓库内统一命名防止任务堆叠job timeout20 分钟给调度延迟留余量retries2 次过滤外部波动保留真故障artifact 保留期7 天防止存储爆炸浏览器矩阵chromium 优先高成本场景下削减浏览器种类6.2 常见问题排查表现象大概率原因解法定时任务整天不触发schedule 被跳过确认近期仓库有活动手动 dispatch 一次任务频繁排不上队并发组内任务积压加短超时或在脚本里加冷却检查同一用例每天挂 3 次以上数据污染或环境时序先用 trace 归因再决定修代码还是修脚本失败概率白天高晚上低被测环境扩缩容或流量变化拉取按小时聚合的失败曲线定位所有用例集体超时runner 到环境链路异常用网络探测 step 做前置过滤必要时上 iperf 测链路7. 高频端到端测试还应该这样继续扩展玩到后面我发现每天 100 次的价值已经超出了找 bug本身。它更像是一台环境健康度的记录仪跑出来的每一次通过率、耗时、延迟这些数据积累起来就是一条质量趋势线。我现在把每次运行的摘要指标追加到一个时序数据库里配合一个简单的看板能直观看到 staging 环境的性能是在变好还是变差。这已经不是测试而是门槛更低、成本更可控的压测方式。另外这套模式衍生出来的一个经验是E2E 的用例数量不是越多越好。高频跑测摊薄了每一条用例的注意力所以用例质量比数量重要得多。我最后把 18 条用例精简到 12 条全都是核心交易链路和最容易出问题的边界场景通过率反而从 97% 提到了 99%。与其跑 100 条无差别点击页面不如跑 12 条真正在意系统心脏的用例。如果你也想搭一套类似的定时巡检我的建议是先别追求 100 次从每天 24 次每小时一次开始跑两周把失败归因流程跑顺再逐步提高频率。高频测试的本质不是跑得多而是每一次失败都要有交代。等你的通知链路、数据隔离、flaky 归因都稳住了再把数字拉满你会看到一套完全不同的质量视图——那正是低频测试永远给不了你的东西。