团队里的测试用例越堆越多了,功能测试、接口测试、AI辅助生成的用例全混在仓库里,很多用例已经失效却还在每天执行。我一直想做个定期自动清理机制,又怕删错东西背锅,最后选了个相对稳妥的路线:用GitHub Actions做定时巡检加回收站机制,让“测试用例自动清理”这件事从手动博弈变成可追溯的自动流程。这篇文章就把我完整踩过的坑、workflow的写法、清理规则的设计思路全盘整理出来,给同样被用例膨胀折磨的测试开发或者CI/CD维护者一个可以直接抄走的参考方案。
1. 为什么测试用例需要自动清理
1.1 测试用例膨胀的三个来源
先说一个行业通识:测试用例是典型的“进来容易出去难”的东西。新功能提测,用例必须补;线上出bug,回归用例就得加;AI辅助生成用例火了之后,团队里用工具一口气能生成几百条覆盖矩阵,听起来很爽,但生成完真的有人逐条review吗?我见到的情况是,多数生成用例合并进仓库之后,既没维护人,也没优先级,唯一的贡献就是让测试套件越跑越慢。
测试用例膨胀的来源,我实际归纳下来主要是三类。
第一类是历史版本遗留。产品迭代到v3.0了,v1.0时代的旧用例还躺在仓库里,相关的功能界面可能早就重构没了,但这些用例没被标记废弃,执行的时候还在跑,红了也没人管。第二类是无效重复用例。同一接口的同一断言逻辑,因为不同测试人员写用例时命名习惯不同,出现了七八个变体,表面上关键词不一样,实际上覆盖的是同一个路径。第三类是“僵尸用例”。这类最讨厌,它们在上个季度可能还是有效的,但最近三个月被测系统的行为变了,用例对应的业务流程下线了,只是没有任何机制通知用例维护者去更新。
这三类用例不清的理由都出奇一致:没人敢删。你说这条用例没用,凭什么?万一以后回归要用呢?这种心理在团队里太常见了,结果就是仓库里的用例只增不减,执行时间从15分钟膨胀到40分钟,维护成本全变成了沉默成本。
1.2 手动清理为什么永远做不完
我试过让测试团队每季度手动清理一次用例,结局基本可以预料:大家都很忙,没人愿意接这个活。因为手动清理是一个“高责任、低收益、难量化”的任务,你要逐条看用例是否有效,要联系当时的编写人确认意图,整个过程全是沟通成本,做完了还没有什么可见的功劳,顶多是你自己知道套件跑得快点。
更现实的问题是,测试用例的“有效性”是会随时间衰减的。今天有效的用例,下一次产品需求变更后可能就失效了。手动清理的节奏永远跟不上业务变化的速度。所以我当时的判断是:我们需要一个能按固定节奏自动跑的清理机制,它不一定一上来就删东西,但至少能把“疑似废弃用例”自动标记出来,让团队成员来做最终决策。这个判断最终指向了GitHub Actions。
2. 方案选型:GitHub Actions凭什么接这个活
2.1 四个候选方案横向对比
做定时自动化清理,市面上方案不少,我第一轮筛选就列了四个:Jenkins、GitLab CI、云函数定时器、GitHub Actions。
Jenkins是传统CI的老大哥,它的定时构建能力很成熟,Pipeline里写Groovy脚本想怎么折腾都行。但它的维护成本摆在那里,需要单独部署服务,还要处理插件升级、节点管理的问题。如果团队没有专职CI运维,用Jenkins跑一个每周一次的清理任务,多少有点杀鸡用牛刀。
GitLab CI的优势是如果代码就托管在GitLab上,CI/CD一条链路很顺,.gitlab-ci.yml里配置rules加schedule也能实现定时任务。但它和GitHub生态的集成比较绕,如果测试用例仓库在GitHub上,还得走webhook或者GitLab的仓库镜像,链路长了故障点就多。
云函数定时器(比如Lambda或者阿里云的函数计算)在“定时触发”这个需求上确实轻量,还能省掉很多维护工作。但是测试用例清理的核心动作是读写仓库文件、创建issue、甚至提交删除的PR,这些操作如果落到云函数里,你得自己写一段调用Git仓库API的代码,还要管理密钥,实用起来并没有想象中简单。
GitHub Actions是最后选的。因为它直接长在GitHub里面,测试用例仓库本来就在那,workflow能直接checkout代码、用官方action操作Issue和PR,权限模型也是原生集成的,整个链条没有任何多余环节。
2.2 我选GitHub Actions的四个决定性因素
第一个决定性因素是cron调度能力。GitHub Actions的schedule事件直接支持POSIX cron表达式,我可以在workflow里写cron: '30 1 * * 5',它就会每周五凌晨1点30分自动跑一次。这个能力对“定期执行清理巡检”的需求几乎是量身定做的。
第二个因素是workflow_dispatch手动触发。定时跑只是一个兜底,真正上线初期,我肯定希望随时能手动跑一次验证效果。只要在触发条件里加上workflow_dispatch,就可以在Actions页面点“Run workflow”按钮,这对调试太关键了。后面聊问题排查时你们会发现,手动触发是我最快定位到问题的重要手段。
第三个因素是GITHUB_TOKEN的权限控制。GitHub Actions内置的token可以控制到contents: write、issues: write、pull-requests: write这种细粒度权限,清理脚本能做的事情是受控的,不会像本地跑脚本时直接拿一个管理员token到处乱操作。这一点对团队协作很重要,因为自动任务权限太大会让人害怕,权限受控的话团队成员才敢放心让定时任务接管。
第四个因素是日志和归档能力。每次workflow运行都会生成完整的运行日志,什么时候执行、哪些用例被标记、哪些操作失败了,全都有据可查。做清理这种“容易引起争议”的操作,可见性比什么都重要。出了问题翻日志就能定位,比在谁电脑上手动跑的脚本靠谱多了。
3. 自动清理到底怎么“清理”才是安全的
3.1 清理规则设计:哪些用例该进回收站
自动清理最怕的一件事就是误删。所以我设计清理逻辑时给自己定了一个红线:GitHub Actions只负责“标记”和“归档”,不直接物理删除任何用例文件。所有看起来疑似废弃的用例,先打上deprecated标签,然后移到一个叫archive/deprecated/的目录下,继续保留在Git历史里。这样就算标记错了,恢复成本也很低,无非移动一下文件再把标签去掉。
我最终采用的清理规则,实际跑了两个多月才稳定下来,经历了三轮调整,核心规则大概是下面四条。
规则一是“长期无更新”。用例文件的最后修改时间超过180天,并且关联的产品需求已经关闭,视为疑似废弃。这个规则对稳定期功能模块下的用例特别有效,因为真实迭代中的用例,只要它还活着,总会因为修bug、补断言、优化数据被改动。180天的阈值不是拍脑袋定的,我拉过仓库的提交记录做统计,有效用例的平均活跃周期基本在3到4个月以内,180天相当于给了两倍的缓冲。
规则二是“连续执行全绿”。对接测试结果平台,查询用例最近30次执行结果,如果全部通过并且期间没有失败记录,这类用例容易被长时间忽略。它们不一定是废弃的,但很可能是“无人关注”的。这类用例我不直接归档,而是生成一份“低关注度用例列表”发到issue里,由维护人手动确认。
规则三是“相同断言去重”。这一步是受到测试用例设计方法中等价类的思路启发。同一接口同一断言条件,如果已经存在一个被实际执行且维护中的用例,那么它的重复变体就会被打上duplicate标签。这个规则单独不删任何东西,只生成重复度报告。
规则四是“标签显式废弃”。如果测试用例文件被显式标记了@deprecated注解(我们在用例的元信息里约定了一个固定的格式),清理任务下一次执行时直接把它移入归档目录。这是人为强制废弃的唯一快速通道,也是唯一会实际移动文件的规则。
3.2 设计方法迁移:等价类思路在清理规则中的应用
谈到测试用例设计方法,大家第一时间想到的可能是写用例时怎么划分等价类、怎么设计边界值。但我发现这些方法倒过来用,恰恰可以变成很好用的清理规则。
等价类的思路迁移过来后是这样的:如果两条用例在“前置条件、执行步骤、预期结果”三个维度上完全等同一个测试目标,它们就属于同一个等价类。保留其中维护最活跃、断言最完整的那条,其余变体标为冗余。我用这个思路清掉了一批重复的接口用例,效果出奇好。以前是三个测试人员各写各的,没人统一标准,现在系统会自动找出重复度超过90%的候选集,由人来挑保留谁。
边界值的思想也可以用:对于那些只差一个参数边界(比如分页大小从10改成20)的用例,如果被测接口的边界行为已经发生变化,这类用例最容易成为“假失败”的来源。清理任务会优先把这类用例提取出来,提醒维护者重新核对边界值,而不是直接标记废弃。这样处理更公平,也更容易让团队成员接受。
3.3 灰度执行:dry-run先行、双人复核兜底
自动清理任务上线前一定要做灰度,这是我的底线。灰度分两层:第一层是dry-run模式,第二层是双人复核。
dry-run模式其实就是一个参数开关。我在清理脚本里加了一个--apply参数,没有这个参数时,脚本只输出“将要做什么”的报告,不实际移动任何文件。第一次跑的时候我用dry-run模式在本地和GitHub Actions里各跑一遍,对比结果一致性。确认无误后,才在workflow里逐步放开执行权限。这一步建议所有做类似自动化清理的同学都不要省,你有再多自信,都不如先让它跑一遍只读模式给你看结果。
双人复核的机制我实现得简单一点:脚本把疑似废弃的用例清单生成到一个Markdown报告里,然后通过GitHub Actions直接创建一个issue,并@对应的维护人。维护人在issue里回复确认,脚本在下一次执行时才能对该清单执行归档。人工确认做得这么轻,就是不想让这个机制变成又一个没人愿意填的流程。
4. 核心实现:workflow和清理脚本拆解
4.1 仓库目录结构与前置准备
先把仓库结构放出来,方便你对照。测试用例仓库的根目录下我分了三块:testcases/放正常维护的用例,archive/deprecated/放废弃归档用例,scripts/放自动化脚本。用例文件统一用YAML格式,每个用例文件头部的元信息字段是清理脚本的输入源,包括title、owner、created_at、updated_at、priority、status、deprecated等。
前置准备阶段要做的事有三件。第一,确认测试用例仓库有GitHub Actions运行权限。第二,在仓库的Secrets里配置好GITHUB_TOKEN(这个其实不用单独配,GitHub Actions运行时自动注入,但权限默认是只读的,需要在workflow里显式声明为可写)。第三,测试结果平台的查询凭证如果涉及到第三方系统,可以放到Secrets里,我这边用一个只读token,专门查用例执行记录。
这里面最容易踩的坑是权限声明。新版GitHub仓库默认情况下,GITHUB_TOKEN的权限是只读的,如果你的workflow要提交代码或者创建issue,必须在workflow文件的permissions块里显式声明contents: write和issues: write,否则会跑一会儿才失败,而且失败原因还不明显。
4.2 定时调度的workflow配置
直接把我最终在用的workflow贴出来,核心结构如下:
name: test-case-cleaner on: schedule: - cron: '30 1 * * 5' workflow_dispatch: permissions: contents: write issues: write pull-requests: write jobs: cleanup: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: pip install pyyaml requests - name: Run cleanup script in dry-run mode env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPORT_MODE: dry-run run: python scripts/cleanup_test_cases.py - name: Run cleanup script with apply if: ${{ runner.debug == '1' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPORT_MODE: apply run: python scripts/cleanup_test_cases.py --apply说几个关键点。schedule里的cron表达式30 1 * * 5表示周五凌晨1点30分触发,这里特别要注意:GitHub Actions的cron使用的是UTC时间,不是你的本地时间,国内用户换算一下相当于周五早上9点半,这个时间恰好避开了开发高峰,比较安全。
workflow_dispatch是调试入口,上线初期建议把--apply的执行步骤先注释掉,只保留dry-run,等日志确认没问题再放开。我在apply这一步的判断条件写的是runner.debug == '1',意思是只有为workflow开debug mode的时候才真正执行删除隔离操作,这样日常巡检永远只读,只有运维人员手动介入时才执行归档,安全系数高很多。
4.3 清理脚本的核心逻辑解析
清理脚本用Python写,主流程分四步:扫描、分析、报告、执行。扫描阶段遍历testcases/目录下所有YAML文件,提取元信息;分析阶段根据前面提到的清理规则逐条打分,为每个用例计算一个“疑似废弃指数”;报告阶段生成一份Markdown报告并创建issue;执行阶段只在带--apply时才真正移动文件。
核心分析逻辑概略是这样的:
import yaml from datetime import datetime, timedelta DEPRECATED_TAG = "deprecated" ARCHIVE_DIR = "archive/deprecated" STALE_DAYS = 180 def analyze_case(case_file: str) -> dict: with open(case_file, "r", encoding="utf-8") as f: case = yaml.safe_load(f) updated_at = datetime.fromisoformat(case.get("updated_at", "2020-01-01")) stale_days = (datetime.now() - updated_at).days reasons = [] if case.get("deprecated") is True: reasons.append("explicit_deprecated") if stale_days > STALE_DAYS: reasons.append(f"stale_{stale_days}_days") return { "file": case_file, "title": case.get("title"), "owner": case.get("owner"), "reasons": reasons, "archivable": len(reasons) > 0, }这个示例做了很大简化,实际跑的时候还会加上重复度检测、执行记录查询等逻辑。有一个细节必须提醒:在dry-run模式里,脚本不要直接创建issue,而是把报告写到reports/目录,通过workflow日志和评论的方式展示。等确认无误后再放开自动创建issue的动作,以免定时任务每天创建一个issue把维护人烦死。
另外,移动文件时不要用os.rename直接改,因为目标目录可能已经存在同名文件。我这边用的是先判断目标是否存在,如果存在就加时间戳后缀再移动,保证不覆盖。归档文件保留原路径的相对结构,方便以后追踪。
5. 常见问题与排查实录
5.1 定时任务不触发:UTC时区是个老坑
第一个遇到的坑就是定时任务不触发。workflow写完push上去,在Actions页面确实能看到这个workflow,但到点没动静。排查半天才发现是时区问题。GitHub Actions的cron用的是UTC,我一开始写的是cron: '30 9 * * 5',本意是北京时间的周五早上9点半,结果它被当成UTC的9点半,相当于北京市时间下午5点半,看起来就像没触发,其实它已经跑过了。
这类问题排查方法很简单:直接在workflow里加一个date的step打印当前时间,确认调度是否正常,后续执行时就能看到真实触发时点。另外GitHub官方文档写得很清楚,cron调度的最小粒度是每5分钟一次,但实际执行可能会有延迟,尤其是仓库不活跃的时候,调度起得会慢一些,这属于正常现象。
5.2 GITHUB_TOKEN权限不够的排查
另一个高频问题是GITHUB_TOKEN权限不够,症状要么是脚本没法push文件,要么是创建issue时报403。我在本地跑脚本完全正常,一扔到Actions里就权限报错,原因就是workflow里漏掉了permissions块。
GitHub仓库的Actions设置里有一项“Workflow permissions”,它有“Read repository contents permission”和“Read and write permissions”两个选项。如果你在workflow里没有显式声明,它会走仓库默认配置。这个默认配置在新仓库里通常是只读的。所以最稳妥的做法是,拿我这个模板去用的时候,把permissions块原样带上。
还有一个不常见但值得记下来的点:如果仓库是fork出来的,GITHUB_TOKEN的权限会受限,某些写操作会被拒绝。这种情况可以考虑使用个人访问令牌(PAT)替代,但PAT的权限范围比较大,不推荐在长期定时任务里用,我自己的方案还是优先仓库手动开启写权限。
5.3 误删之后怎么恢复
哪怕是灰度再严格,真跑起来之后误标还是会发生的。有一批用例因为“连续全绿”被标记成低关注度,但实际上是某个核心模块的冒烟用例,只是那个月没人推进需求变更而已。还好我们做的是“归档”而不是“物理删除”,恢复只在几十秒内就能搞定。
恢复流程是:从归档目录将文件移回testcases/对应路径,去掉元信息里的deprecated标签,然后提交一个revert的PR。如果团队里有多人同时操作,最保险的方式是直接用git log --follow追踪文件的移动记录,找到归档提交的hash,然后git revert <commit>。因为归档动作是单独的提交,revert之后文件就能回到原始状态,非常干净。
这个过程让我重新认识到一个原则:自动清理任务只做“安全的移动”,永远不要执行git rm或者物理删除文件。保留历史,就是保留后悔药。
5.4 从“C盘自动清理”到“VIP路由漂移”:自动清理的两个教训
热词里提到C盘自动清理,还提到keepalive的VIP漂移后路由不会自动清理,这两个和测试用例清理看着无关,但其实道出了自动清理领域的两个通用教训。
C盘自动清理的教训是:默认的清理逻辑往往很粗暴。系统自动清理只判断文件大不大、新不新,不会判断文件是不是还有用,结果就是清理完了,用户发现某个软件配置没了或者缓存导致功能异常。映射到测试用例清理,就是不能只看修改时间就把用例移走,必须结合执行频率、关联需求状态、维护人确认这些业务信息,才能做出“有用还是没用”的判断。
keepalive的VIP漂移后路由不会自动清理这个场景更有意思:它不是智能不足,而是“该清的时候没清”。VIP都漂移走了,旧路由信息还留在路由表里,这种残留信息会干扰后续的流量转发。其实测试用例也一样,功能已经下线了,用例还留在套件里每天执行,轻则浪费执行时间,重则因为“该失败却通过”把问题掩盖掉。所以自动清理的核心价值不是“删得快”,而是“在正确的时间把失效的资产识别出来”。
这两个场景印证了我前面反复强调的东西:自动清理工具只负责“行动的自动化”,清理策略本身必须靠人的经验来沉淀。GitHub Actions只是让策略可以定时、可追溯地执行,真正让清理不误伤的逻辑,全都写在规则设计里。
6. 自动清理“有漏洞吗”?一个需要认真回答的问题
我在调研时看到有人问camunda 7.17.0自动清理有没有漏洞。虽然camunda的流程历史数据清理和GitHub Actions清理测试用例不是同一套技术栈,但这个问题背后的担忧是相通的:自动清理机制,会不会因为配置不当或者机制不成熟,反而把不该清的数据清了?
我的答案分两层。第一层,工具本身基本是可靠的。GitHub Actions的cron调度、文件读写、权限控制都非常成熟,稳定跑了几个月没出过故障。第二层,真正可能有漏洞的是清理策略本身。比如我早期把“180天未更新”作为单一判据,结果误标了一批长期稳定的核心用例。这不是工具漏洞,是我的规则设计有漏洞。
所以处理这个担忧的方法不是放弃自动清理,而是给自动清理加上“策略保护层”,我们已经在前面聊过的dry-run、归档机制、双人复核、可恢复性都是保护层。只要保护层足够完善,哪怕规则设计出问题,最差的结果也只是“多归档了几条用例”,不会造成不可逆的破坏。对于做质量保障的团队来说,这个心态很重要:自动化的目的不是零风险,而是把风险控制在可恢复的范围内。
6.1 跑了一个季度之后我看到了什么
清理任务按每周频率跑了一个季度后,数据是能明显说明问题的。测试套件整体执行时间从40分钟左右降到了25分钟内,因为被移除的很大程度上是重复或者失效的用例。仓库里的用例文件数量基本稳定在一个水平,没有继续往上飙。最大的变化是团队的维护信心,以前大家对“用例该不该清”是躲着走,现在每个季度会主动看一次自动生成的清理报告,讨论哪些规则要调整。这个机制从“我一个人维护的脚本”慢慢变成“团队认可的质量资产”,算是意外收获。
6.2 这个方案后续还能怎么扩展
目前这套GitHub Actions清理机制只覆盖了用例文件的归档和标记,后续要扩展的第一步是跟测试结果平台做更深的联动。比如把用例最近30天的执行结果、失败时的报错信息、关联缺陷的修复状态拉进来,让清理规则的判断维度从“文件静态属性”升级为“用例全生命周期健康度”。第二步是把清理报告接入团队的IM通知,这样每周巡检结果不用等人工去翻issue,直接推送给所有用例维护人。第三步是把这个机制复制到接口测试用例和性能测试脚本的仓库,清理规则大同小异,脚本复用成本很低。
我现在还没有把物理删除放到自动流程里,存档的用例会永久留在归档目录。等到团队对这个机制建立足够信任后,再考虑增加“二次确认后物理清理”的阶段,毕竟Git仓库体积也是成本之一。这条路不着急,慢慢走稳比走快更重要。