做RFRobot Framework自动化测试的同学大概率都遇到过这种情况一个用例昨天还好好跑过今天CI持续集成上突然红了一波你点开日志一看定位元素超时、网络抖动、环境变量没生效压根不是代码逻辑的问题。你手动重跑一遍全绿。这种“偶发失败、重复成功”的用例业内叫flaky test中文场景里大家更习惯叫“不稳定用例”。如果测试报告常年被这种失败污染团队对自动化结果的信任度就会直线下降最后变成“红了也没人看绿了也没人信”的摆设。这篇文章想聊的就是我在实际项目中落地的一套Robot Framework失败自动重跑机制。我会先把为什么要做重跑、哪些用例值得重跑讲清楚再对比Robot Framework自带的参数方案和它解决不了的问题最后给出一个可以抄作业的完整实现包括Python调度脚本、结果合并、Jenkins集成以及我在这个过程中踩过的坑和排查思路。无论你是在搭第一版RF框架还是正在被一堆flaky用例折磨这篇内容都能给你一个明确的方向。1. 为什么要做失败重跑被“偶发失败”折磨过的人都懂1.1 “重跑”解决的真实痛点自动化测试执行一次全量用例通常要跑十几分钟甚至几个小时。在这个时间段里被测系统的状态、依赖服务、网络环境都在动态变化不可能做到每次执行环境和真实线上环境完全一致。所谓“偶发失败”往往就是这种环境差异导致的比如测试环境接口偶发超时一次请求超过2秒就报错但重试一次就恢复页面元素加载顺序不稳定自动化脚本执行太快元素还没渲染出来就去找它Chrome浏览器自动更新到新版本旧版Selenium驱动没法适配前置用例产生的脏数据影响了后置用例的结果CI机器资源紧张执行过程中CPU飙升导致用例响应超时。如果这些偶发失败直接就报红测试同学就得逐条去翻日志判断到底是代码回归还是环境问题这个时间成本非常可观。更麻烦的是失败结果进入统计报表后会让自动化通过率虚低长期下来管理层会对自动化测试的价值产生质疑。引入失败重跑机制以后执行策略变成了“先跑一遍全量失败用例自动挑出来再跑一遍”。如果第二遍过了就标记为“不稳定后恢复”不再当作失败项看待只有重跑仍然失败才进入真正的缺陷排查流程。这一套逻辑能有效过滤掉环境类噪声让最终报告更接近被测代码的真实质量。1.2 可重跑性评估不是所有用例都该重跑在动手写重跑脚本之前必须先想清楚一个边界问题重跑到底应该覆盖哪些用例。如果所有失败用例统统无脑重试会带来两个新问题。第一执行时间翻倍失去自动化提效的意义。第二掩盖真实缺陷有些用例第一遍失败确实是bug重跑又成功只是因为碰到了不同的数据分支但这个问题如果被重跑机制“洗白”了回归风险反而升高了。所以我定的原则是三类用例不重跑明确断言型失败不重跑。比如登录后校验用户名不一致、接口返回码不符合预期这种失败已经能说明功能逻辑出了问题重试十次结果也一样。初始化环境失败不重跑。如果套件启动时数据库连接失败、依赖服务没起起来那属于环境级别故障重跑用例本身没有意义应该先修环境。有数据唯一性约束的用例不重跑。比如注册用例已经插入了相同手机号重跑时会因为主键冲突再次失败这种用例需要的是数据清理而不是重试机制。真正适合重跑的是那些失败信息里带Timeout、ConnectionError、ElementNotVisible这类关键字或者用例本身有明确的环境依赖但又不稳定的场景。这块的判断我要在重跑脚本里做一层过滤不能全量重试。1.3 重跑的最终目标让报告反映真实质量有些团队做重跑只是为了把通过率刷好看报告里把重跑结果直接覆盖原始失败连个不留痕。这种做法短期里数据好看了但长期来看是自欺欺人因为缺陷在重跑过程中被掩盖了没有任何人知道这条用例第一遍是失败的。我的做法和这个相反。重跑不是用来“洗白”失败的而是用来区分失败类别的。一条用例第一遍失败、第二遍成功它仍然说明存在不稳定因素需要在报告里单独标注“flaky”提醒测试同学关注这个模块的稳定性一条用例重跑后仍然失败那就是确凿的缺陷需要进入bug管理流程。这样设计后重跑机制不是用来掩盖问题的而是为问题分类服务的。2. RF自带的重跑机制能用但别指望它解决所有问题2.1 先认识robot命令的官方参数Robot Framework从3.0版本开始就内置了失败用例重跑的参数支持核心是两个命令项--rerunfailed和--rerunfailedsuites。这个参数的设计思路很直接第一条命令执行产生output.xml文件第二条命令读取这个文件里的失败结果自动组装一个新的测试集合来执行。以实际命令为例# 第一次全量执行 robot --outputdir results tests/ # 读取第一次结果中的失败用例单独重跑 robot --rerunfailed results/output.xml --outputdir results/rerun tests/ # 合并两次结果 rebot --outputdir results --merge results/output.xml results/rerun/output.xml--rerunfailedsuites则是按套件粒度重跑如果某个套件里有任何失败整个套件重新执行一遍。这种方式的优点是执行逻辑更完整因为套件级别的setup和teardown会重新走一遍但是执行时间也会更久。另外还有一个常见搭配是--exitonfailure第一次跑的时候遇到失败就停止。这个参数通常在调试阶段使用正式回归时不建议全量用例开启因为一旦失败就会跳过剩余用例重跑的意义就不大了。2.2 官方方案的完整流程演示我用一个具体例子演示官方参数怎么配合使用。假设项目目录结构如下tests/ login.robot order.robot user.robot第一次执行全量用例robot --outputdir results tests/执行结束后results/目录下会生成output.xml、log.html、report.html。其中output.xml是重跑的关键数据源里面记录了每条用例的执行状态和失败信息。接着读取失败用例执行重跑robot --rerunfailed results/output.xml --outputdir results/rerun tests/这条命令会从output.xml中解析出所有失败用例的ID按顶层套件路径组装成一个新的测试集然后只运行这些失败用例。重跑结果单独写到results/rerun/output.xml不会覆盖第一次的结果。最后合并报告rebot --outputdir results --merge results/output.xml results/rerun/output.xmlrebot是Robot Framework的测试结果合并工具--merge参数会把重跑结果合并进原报告。合并后打开report.html可以看到每条用例的状态如果重跑成功最终状态会更新为 PASS同时保留“上一次失败”的痕迹标记。这套流程看起来已经能解决问题了对吧但实际上手以后就会发现几个明显短板。2.3 官方方案的三个明显短板第一个短板是缺少重跑次数控制。官方参数要么不重跑要么只重跑一遍没有“重跑两次仍失败才判定失败”的选项。对于网络环境抖动频繁的项目一遍重跑不足以过滤噪声。第二个短板是结果合并后会丢失失败现场。--merge虽然能把最终状态更新为通过但原始失败在合并后的 report 里只剩一个标记失败截图、堆栈信息都被弱化了。后续如果想分析这条用例为何偶发失败还得回到第一次的 log.html 里翻实际操作起来很麻烦。第三个短板也是最核心的官方方案重跑仍然失败时不会自动做一个“最终判定”。它只是把两次结果合并在一起最后一条用例展示成什么状态取决于rebot合并的先后顺序测试报告无法直观区分这是“稳定通过”还是“重跑后通过”还是“重跑仍失败”三类结果。提示如果你只想快速给现状打一个补丁官方--rerunfailedrebot --merge已经可以应付简单场景。但如果你的项目用例数量超过两百条或者已经出现了明显的flaky趋势我建议往下看自己写一套重跑调度逻辑把主动权握在自己手里。3. 基于RF自动化重跑的完整方案设计与实现3.1 方案整体架构一次执行失败筛选定向重跑结果合并既然官方方案有短板我就按自己的需求重写了一套基于Python脚本的重跑机制。整体流程分为四个阶段第一阶段执行全量测试产出标准output.xml。第二阶段解析output.xml筛出失败用例并根据失败原因排除“断言失败型”用例。第三阶段对筛选后的失败用例执行定向重跑最多支持配置重跑次数。第四阶段合并结果生成三类统计——原始通过、重跑后通过、最终失败。这套设计的核心思路是“分层处理”第一遍的结果是真实执行的原始数据必须保留重跑结果单独存放最后合并展示。这样既保证了数据的可追溯性又能让报告一目了然。3.2 Python调度脚本实现核心重跑逻辑调度脚本是整个方案的心脏。它负责解析RF结果、调用robot命令、控制重跑次数、归类最终状态。我用Python的xml.etree.ElementTree来解析output.xml用subprocess来调用robot和rebot命令整体逻辑清晰可控。先看核心代码框架import os import sys import subprocess import xml.etree.ElementTree as ET from datetime import datetime class RFRerunManager: def __init__(self, test_dir, output_dir, max_rerun2): self.test_dir test_dir self.output_dir output_dir self.max_rerun max_rerun os.makedirs(output_dir, exist_okTrue) def run_full(self): 执行全量测试 cmd [ robot, --outputdir, self.output_dir, --output, output.xml, self.test_dir ] subprocess.run(cmd, checkFalse) return self._parse_failed_tests(output.xml) def _parse_failed_tests(self, xml_name): 解析output.xml提取失败用例信息 xml_path os.path.join(self.output_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() failed_cases [] for test in root.iter(test): status test.find(status) if status is None: continue if status.attrib.get(status) FAIL: failed_cases.append({ id: test.attrib.get(id), name: test.attrib.get(name), suite: self._find_suite_name(test), message: status.text }) return failed_cases def _find_suite_name(self, test_elem): 向上查找所属套件名称 parent test_elem.find(..) while parent is not None and parent.tag ! suite: parent parent.find(..) if parent is None: return return parent.attrib.get(name, ) def filter_rerunnable(self, failed_cases): 过滤掉不适合重跑的用例 non_rerun_keywords [ AssertionError, Expected.*to be, should be equal, can not be empty, must be provided ] rerunnable [] for case in failed_cases: message case[message] or should_skip False for keyword in non_rerun_keywords: if keyword.lower() in message.lower(): should_skip True break if not should_skip: rerunnable.append(case) return rerunnable def run_rerun(self, failed_cases, rerun_round1): 对指定用例执行重跑 if not failed_cases: return test_ids [case[id] for case in failed_cases] output_name frerun_{rerun_round}.xml # 组装 --test 参数列表 cmd [robot, --outputdir, self.output_dir, --output, output_name] for test_id in test_ids: cmd [--test, test_id] cmd [self.test_dir] subprocess.run(cmd, checkFalse) def merge_results(self, round_count): 合并全量结果和每一轮重跑结果 merge_args [ rebot, --outputdir, self.output_dir, --output, final_output.xml, --merge, os.path.join(self.output_dir, output.xml) ] for i in range(1, round_count 1): merge_args.append(os.path.join(self.output_dir, frerun_{i}.xml)) subprocess.run(merge_args, checkFalse) def run(self): 主流程 print(f[{datetime.now()}] Step 1: 执行全量测试) fail_cases self.run_full() print(f[{datetime.now()}] 失败用例数: {len(fail_cases)}) print(f[{datetime.now()}] Step 2: 过滤可重跑用例) rerunnable_cases self.filter_rerunnable(fail_cases) print(f[{datetime.now()}] 可重跑用例数: {len(rerunnable_cases)}) final_status [] # 记录最终失败用例 final_failed [] for case in rerunnable_cases: case_passed False for round_no in range(1, self.max_rerun 1): print(f[{datetime.now()}] 重跑第 {round_no} 轮: {case[name]}) # 构造单用例重跑命令 cmd [ robot, --outputdir, self.output_dir, --output, fsingle_rerun_{round_no}.xml, --test, case[name], self.test_dir ] result subprocess.run(cmd, capture_outputTrue, textTrue) # 不解析XML直接通过返回值判断是否执行成功 # robot命令退出码与状态对应0全过1有失败2异常 if result.returncode 0: case_passed True break if case_passed: final_status.append((case[name], RERUN_PASS)) else: final_status.append((case[name], FAILED)) final_failed.append(case[name]) self.print_summary(final_status) print(f[{datetime.now()}] 最终失败: {final_failed}) if __name__ __main__: manager RFRerunManager( test_dirtests/, output_dirresults/, max_rerun2 ) manager.run()这段脚本有几个设计细节需要重点说明。第一--test参数可以重复指定多个用例Robot Framework会精确匹配这些用例名执行顺序和参数顺序一致。这个特性天然适配“按失败用例定向重跑”的需求比整文件重跑效率高很多因为套件里其他通过了的用例不会参与第二次执行。第二subprocess.run的capture_outputTrue可以捕获robot命令执行时的日志输出方便在CI阶段追踪脚本运行情况。重跑过程中我们不需要逐条用例解析xml来判断通过还是失败直接看命令的返回码就够了0代表全过1代表有失败2代表执行过程异常。这个逻辑可以节省大量解析时间。第三filter_rerunnable方法里的关键词过滤列表是我根据实际项目的失败信息提炼的。Robot Framework的断言失败通常会在message里带Expected、should be equal这类字样这类失败重跑没有意义直接跳过能避免无谓的执行时间浪费。需要注意一点重跑命令中用了--test按用例名过滤后Robot Framework仍然会先执行套件级别的Suite Setup。如果你套件的setup里有注册数据、初始化账号等操作就需要确认这些操作是否幂等否则第二次重跑时会因为数据重复而失败。3.3 测试报告合并与清理脚本最后要解决的是结果呈现问题。我用rebot --merge把第一轮全量结果和每轮重跑结果合并生成最终的final_output.xml和配套的log.html、report.html。这样报告里既能看到用例第一遍执行的情况也能看到重跑后的最终状态。但前面提到过单纯依赖--merge无法清晰区分三类状态。所以我在脚本里额外输出了一份文本格式的执行摘要统计信息: 全量用例总数: 128 首轮失败数: 12 可重跑数: 9 重跑后通过数: 7 最终失败数: 2 不稳定用例列表: - 用户登录_记住密码_001 - 订单创建_库存校验_005这份摘要会同步输出到控制台和日志文件Jenkins构建后可以直接从控制台日志里提取关键信息不用打开HTML报告就能知道回归结果。文件清理方面我建议每轮产物不要直接覆盖。因为排查问题时你往往需要回到具体某一轮的执行结果里看当时的截图和堆栈。我当前项目的做法是保留output.xml、每轮rerun_N.xml、final_output.xml每次构建开始时会清理上一轮的产物防止磁盘膨胀。如果项目需要长期留痕可以考虑再挂一层归档策略。3.4 集成Jenkins与Allure报告脚本本身已经能独立落地但要真正融入研发流程还需要和CI系统接线。我这边主要用Jenkins接入方式很直接。在Jenkins新建一个自由风格任务构建步骤选择“执行Shell”或者“执行Windows批处理命令”把调度脚本的启动命令写进去即可python rerun_manager.py如果想让RF的报告直接展示在Jenkins任务页面上可以安装Robot Framework Plugin插件在“构建后操作”里添加“Publish Robot Framework test report”填好output文件路径。这样每次构建完成后Jenkins页面会直接渲染RF的log和report团队成员不用进入工作区翻文件。如果团队用Allure做统一测试报告中心也可以在重跑流程执行完后生成Allure结果robot --listener allure_robotframework --outputdir results tests/ allure generate results/allure-results -o results/allure-report --clean注意allure_robotframework是一个独立的第三方监听器库安装方式pip install allure-robotframework它的作用是在RF用例执行过程中实时采集步骤、截图、日志等数据最终生成为Allure格式的结果文件。Allure报告的好处是它可以按历史构建生成趋势图能直观看到某个用例在不同构建中的通过率和重跑次数这对分析flaky用例非常有帮助。4. 实操过程与关键步骤复盘4.1 搭建示例项目一个带失败用例的RF测试套件理论说再多不如把整个流程跑一遍。我先创建一个最小化示例项目用来验证重跑脚本的逻辑。项目结构如下demo/ tests/ login.robot order.robot run.py results/login.robot里故意设计两个用例一个稳定失败一个偶发失败*** Settings *** Library Collections *** Test Cases *** 登录成功_正常流程 ${resp} Set Variable success Should Be Equal ${resp} success 登录失败_密码错误断言 ${resp} Set Variable error Should Be Equal ${resp} success第二个用例明显是断言失败按照我们前面设计的过滤逻辑它应该被排除在重跑范围外。再建一个order.robot模拟不稳定用例*** Settings *** Library DateTime *** Test Cases *** 订单创建_网络超时模拟 ${current_second} Get Current Date result_format%S Log current second is ${current_second} # 模拟偶发场景秒数大于20时用例失败 Run Keyword If ${current_second} 20 Fail simulated network timeout Log 订单创建成功这个用例用当前时间的秒数来模拟偶发失败跑到秒数大于20时触发失败否则通过。真实项目里当然不会这样写但用来验证重跑机制非常合适——每次执行时间不同结果就不同。4.2 从零开始配置重跑脚本按照前面贴出的RFRerunManager类加上一个实例化入口if __name__ __main__: manager RFRerunManager( test_dirtests/, output_dirresults/, max_rerun2 ) manager.run()在项目根目录执行python run.py控制台输出的执行流程大致是第一步全量执行会在results/生成output.xml第二步脚本解析XML提取失败用例第三步过滤掉断言类失败用例剩下的记录重跑第四步逐条调用robot命令重跑每轮轮询记录结果第五步打印汇总信息合并最终报告。我在本地跑了几轮输出序列基本符合预期。偶发用例在秒数大的时候第一遍失败重跑一轮就过了如果是秒数小的时候执行它就是第一遍直接通过。稳定失败的断言用例则一直不被纳入重跑范围最终失败列表里直接出现它。4.3 并发重跑与性能优化按上一版脚本逐条重跑遇到失败用例多的时候执行时间会拖得比较长。比如某天环境抽风一次挂了30条用例重跑一轮就要把30条用例从头依次执行效率不可接受。针对这个场景我做了两处优化。第一处是把逐条重跑改为分组重跑将可重跑用例按套件分组每个套件一批执行利用--test参数一次传入该套件下的多个失败用例。这样既保留了定向性又减少了robot进程的启动次数。第二处是引入pabot工具做并行执行这是一款Robot Framework的并行测试执行工具安装方式pip install -U robotframework-pabot使用pabot执行重跑时命令变成了pabot --outputdir results/rerun --test case1 --test case2 tests/pabot默认会按CPU核心数并行执行测试套件能显著压缩重跑时间。但引入并行后要注意两个问题一是用例之间的数据隔离如果在同一数据库schema下操作并行极易产生数据冲突二是UI自动化用例并发时对测试机资源占用敏感并发数过大可能反而导致更多超时失败。我在实际项目中的参数是--processes 4把并发控制在4个进程以内执行时间大约能缩短40%到50%同时不会明显提高flaky概率。如果你的场景里用例本身对数据隔离要求高建议先分组再并行或者干脆保持串行安全优先。5. 常见问题速查与经验避坑5.1 真正调试重跑机制时遇到的坑坑一--test匹配到多个同名用例Robot Framework的--test参数支持通配符同时也支持精确匹配。如果不同套件下存在同名用例比如login.robot和admin_login.robot里都有登录成功这个用例名那指定--test 登录成功时两个用例都会被执行重跑结果就串了。解决方法有两种一是用例命名时带上前缀比如用户端_登录成功、管理端_登录成功从源头避免重名二是在--test参数里使用套件路径限定例如robot --test tests.login.登录成功 tests/坑二重跑时套件setup重复执行导致数据问题Robot Framework执行指定用例时套件级别的Suite Setup和Suite Teardown仍然会运行。如果setup里有造数逻辑重跑时相当于二次造数可能出现唯一键冲突。我的处理办法是在setup里加一个“先清理再生产”的逻辑或者把造数操作迁移到失败率最高的用例自身的setup里配合数据清理关键字使用。涉及第三方系统交互时宁可每个用例独立准备数据也不要依赖套件级别共享。坑三rebot合并后打开报告白屏--merge对output.xml有版本兼容要求如果第一次执行和重跑使用的Robot Framework版本不一致合并时会提示无法解析xml。尤其在使用CI工具拉取依赖时要注意锁定robotframework版本pip install robotframework7.0.1坑四Jenkins执行shell时找不到robot命令Jenkins的slave节点如果Python环境是pyenv或者virtualenv管理的在构建任务里执行robot命令可能会报“command not found”。这是因为Jenkins的PATH环境变量里没有包含Python虚拟环境的bin目录。解决方法是在构建脚本里先激活虚拟环境cd ${WORKSPACE} source venv/bin/activate python rerun_manager.py坑五重跑逻辑与CI插件配置冲突Robot Framework Plugin在构建后操作里自己也会分析output.xml如果重跑脚本同时用rebot生成了新output文件插件默认读取的可能是旧文件两者结果对不上。我现在的做法是脚本最终统一输出一份final_output.xmlJenkins插件里直接指定该文件作为报告数据源。这样不管中间跑了多少轮插件解析的都是最终合并结果不会出现数据不一致。5.2 RF重跑避坑清单下面这张表是我从多次实践中整理出来的注意事项直接照抄能省不少排查时间项目建议原因重跑次数设置为1到2次超过2次执行时间翻倍收益急剧下降重跑范围仅过滤环境类失败断言失败重跑没有意义反而掩盖问题报告合并保留首轮结果加合并结果原始失败现场是分析问题的关键凭据用例命名全局唯一避免同名防止--test参数一次匹配多个用例套件setup必须是幂等操作重跑时setup会再次执行非幂等会制造脏数据并发执行不超过4个进程超过后资源竞争导致更多flaky失败版本控制锁定Robot Framework版本避免版本升级后output.xml不兼容日志输出记录每一轮执行的returncode方便在CI日志中快速定位是哪一轮失败5.3 如何判断重跑机制本身没有引入问题重跑机制上线后还需要一套校验方法确认它没有掩盖真实缺陷。我的做法是每周做一次“重跑效果分析”重点看三个指标重跑通过率如果重跑后通过率超过90%说明第一次失败大概率是环境噪声机制有效如果重跑后通过率长期低于50%说明失败模式不是环境问题而是用例设计问题应该去修用例而不是改重跑逻辑。重跑触发率如果每天触发重跑的用例数量特别高比如超过20%说明测试环境本身的稳定性出了问题重跑只是临时缓解根本治理方向是改善环境稳定性。重跑后失败用例分析每周固定review最终失败的用例判断是不是真实bug如果是跟踪开发修复情况形成闭环。这三项指标结合起来可以让重跑机制持续进化而不是永远停留在“无脑重跑”的粗放阶段。最后再分享一点我的个人体会做自动化重跑这一年多最大的感受是重跑机制不能孤立设计它必须和测试报告、CI流程、用例设计三者打通。很多人把重跑理解为“给robot命令加个参数”但实际上它是整个自动化稳定性体系里的一环。重跑参数设置成多少、过滤逻辑怎么写、结果如何展示这些问题背后反映的是团队对“什么算失败”的定义是否清晰。如果你现在正被一堆flaky用例折磨我建议你先别急着写脚本。花半天时间把最近两周的失败日志翻一遍给失败原因分个类你会发现大部分“偶发失败”其实都有规律可循只是以前没人统计。做完分类再去设计重跑策略你的方案一定会比直接抄网上的脚本更贴合自身项目。另外重跑只是兜底手段核心还是要把用例设计得足够稳定。如果重跑通过率长期偏高不要庆幸反而该警惕环境债是不是越欠越多了。