做接口测试这些年,我见过太多自动化项目死在“维护”这两个字上。脚本写的时候雄心勃勃,跑起来也像模像样,但业务一变动、参数一调整、接口一升级,原本能稳定执行的用例开始集体“失灵”,最后变成谁也不敢改、谁也不想碰的遗留物,只能推倒重来。后来我换了个思路:把用例从代码里解放出来,用Excel管理场景和数据,用Robot Framework只负责执行和断言。这套“Excel + Robot Framework”的组合,看起来不够炫,却在落地效率、协作成本和长期维护性上,实打实地解决了我前面说的那些问题。
这个方案特别适合三类人:一是团队里有人会写自动化脚本、但不想整天陪业务改代码的测试开发;二是业务测试人员能看懂Excel、想参与用例维护,但不想学编程;三是项目接口数量中等、接口变动频繁,需要快速跟上迭代节奏的团队。下面我会把环境搭建、用例模板设计、读取驱动、断言回写、常见坑排查这一整套流程完整拆开,给你一份可以直接抄作业的落地参考。
1. 为什么用Excel加Robot Framework来管接口自动化
1.1 多数接口自动化方案的痛点
先说说我现在对这个问题的认识。常见的接口自动化方案,无论是用Python的requests直接写脚本,还是用Postman导出的Collection再转代码,做出来的东西多少都会碰到几个绕不开的坎。
第一个坎是“用例即代码”。当你把几十条、上百条用例写成脚本以后,用例的维护动作就绑死在写代码的人身上了。业务测试人员想加一条用例,只能提需求等你改;你休假了,需求就堵在那。等你回来改完,可能又要处理脚本之间的依赖,牵一发而动全身。
第二个坎是“参数变化频繁”。接口字段一旦调整,你要先去代码里定位对应的地方,再判断哪些用例受影响,改完之后还要重新跑一遍确认没有影响别处。这个过程极其耗时,而且容易漏。
第三个坎是“断言逻辑不透明”。脚本里写着很多东西只有自己能看懂,别人评审的时候看着一堆代码,很难判断覆盖是否完整,也很难验证断言是否有效。时间一长,用例为什么这么写、当初要防什么bug,谁也说不清,沦为看起来自动化了、实际靠人肉回归的摆设。
1.2 Excel加RF这个组合怎么绕开这些坑
用Excel管理用例,核心变化是让“用例”和“执行引擎”彻底分离。Excel表格里存的是什么接口、什么参数、什么预期结果,团队里任何人都能直接看、直接改。Robot Framework作为一个关键字驱动的自动化框架,本身对数据驱动的支持非常自然,它不要求你把业务逻辑写成包成一团的代码,而是鼓励你把“发请求”“做断言”“取字段”这类动作拆成关键字,再通过数据去驱动它们。
我实际用下来,这个组合带来的最直接好处有三个:
- 用例维护成本大幅下降:新用例就是在Excel里复制一行改数据,不用动代码文件。
- 沟通成本下降:测试人员可以拿着Excel跟开发核对接口字段,不用把代码截图发来发去。
- 自动化逻辑可以沉淀:请求怎么发、断言怎么下,这些逻辑封装在关键字里,团队内可以复用,不用每个人各写一套。
1.3 适用场景与边界
当然,这个方案不是万能药。它更适合中小规模的WebService接口测试场景,比如内部系统的REST接口、SOAP接口、或者对外提供的HTTP服务。如果你的团队有上百个微服务、接口调用链非常复杂,或者需要高并发压测级别的验证,那更适合引入专门的接口测试平台或者更重型的框架。
我自己的经验是,先想清楚自动化要解决什么核心问题。如果核心诉求是“守住核心接口的回归质量,并且用例能跟着业务快速调整”,那么Excel加Robot Framework就是很务实的切入点。千万别一上来就想追求脚本写得多么抽象、平台化做得多完整,先把日常最痛的那部分覆盖住,后面再逐步扩展。
2. 动手前的准备:环境和目录结构
2.1 安装依赖,三行命令搞定
在Python环境准备好之后,安装Robot Framework和配套库只需要几条命令。我用的是Robot Framework 6.x系列,配合requests库的封装版本robotframework-requests,这两个是目前比较稳定的组合。
pip install robotframework pip install robotframework-requests pip install openpyxlrobotframework是主框架,robotframework-requests提供了在Robot里直接发HTTP请求的库,openpyxl是我用来读写Excel的Python库。之所以用openpyxl而不是直接用Robot里的ExcelLibrary,是因为我需要在读取用例的同时做一些字段清洗和格式化,比如把Excel里读出来的状态码统一成字符串、把请求体从文本转成字典,这些逻辑写在自己封装的Python库里比堆在Robot关键字里舒服得多。
安装完成后,可以顺手验证一下环境:
robot --version能正常输出版本号,环境就算通了。如果遇到pip安装慢的问题,记得换成国内镜像源,不然下载robotframework的时候会等到怀疑人生。
2.2 目录结构规划
项目结构我习惯这样放,简洁清楚,也方便后面加内容:
api_auto_test/ ├── testcases/ │ └── api_suite.robot ├── resources/ │ └── ExcelDataReader.py ├── testdata/ │ └── api_cases.xlsx ├── output/ │ ├── log.html │ ├── report.html │ └── output.xml └── run.sh- testcases目录放Robot Framework的套件文件,也就是执行入口。
- resources目录放自定义的Python工具库,比如读取Excel、提取JSON路径、回写结果。
- testdata目录放Excel用例文件,日常改用例只需要动这个文件。
- output目录放Robot Framework自动生成的日志和报告。
run.sh是启动脚本,里面就是一行robot命令:
robot -d output testcases/api_suite.robot把目录结构固定下来,团队成员接手的时候就不会出现“用例文件放哪、报告去哪找”的混乱。很多人做自动化容易忽略这一点,其实清晰的项目结构本身就能降低维护门槛。
3. 用例模板设计,先从一张Excel开始
3.1 表头字段怎么定
Excel用例模板是整个方案里最值得花时间设计的地方。字段少了,写用例的时候会发现很多东西无处安放;字段多了,维护起来又会觉得繁琐。我在多个项目里反复调整之后,沉淀出下面这一套比较通用的字段组合:
| 字段 | 说明 |
|---|---|
| 用例编号 | 唯一标识,方便在报告和日志中定位 |
| 用例名称 | 描述这条用例要验证什么场景 |
| 是否启用 | 值为“是”才会执行,留作临时跳过用例 |
| 请求方法 | GET/POST/PUT/DELETE等 |
| 请求路径 | 接口路径,不含域名部分 |
| 请求头 | JSON字符串,例如 {"Content-Type":"application/json"} |
| 请求体 | JSON字符串,POST/PUT时使用 |
| 预期状态码 | 例如200、201、400、404 |
| 预期JSON路径 | 需要额外校验的响应字段路径,例如 data.user.name |
| 预期值 | 上面JSON路径对应的期望值 |
| 超时(秒) | 单条用例请求超时时间,默认可以留空 |
| 实际状态码 | 执行后回写 |
| 执行结果 | PASS或FAIL,执行后回写 |
| 错误信息 | 断言失败或请求异常时回写 |
你可能会问,请求头、请求体写成JSON字符串,执行时再转成对象,这是不是有点绕?直接固定死不行吗?
我的考虑是这样:接口测试的灵活度恰恰来自这里。不同接口的请求头千差万别,有的要带Token,有的要带不同的Content-Type,有的要传文件,把这些都设计成Excel里单独的列,表格会变得非常臃肿。用JSON字符串放在一个单元格里,既保留了格式自由度,又不会撑爆表格结构。执行的时候解析JSON字符串即可。
3.2 填写规范与示例数据
下面是一个两行示例数据,方便直观理解:
| 用例编号 | 用例名称 | 是否启用 | 请求方法 | 请求路径 | 请求头 | 请求体 | 预期状态码 | 预期JSON路径 | 预期值 |
|---|---|---|---|---|---|---|---|---|---|
| TC001 | 正常查询用户 | 是 | POST | /api/user/query | {"Content-Type":"application/json"} | {"userId":"1001"} | 200 | data.userName | 张三 |
| TC002 | 用户不存在 | 是 | POST | /api/user/query | {"Content-Type":"application/json"} | {"userId":"9999"} | 200 | data.message | 用户不存在 |
有几个细节需要养成习惯。请求路径里不要带环境相关的域名,域名放到Robot的变量里统一管理。请求头和请求体必须是合法的JSON格式,少一个双引号都会导致后续解析直接报错。预期JSON路径我用的是一种简化写法,用点号分隔层级,比如data.user.name就表示响应体里data对象下的user对象下的name字段。
注意:预期状态码在Excel里可能是数字格式,读出来之后会变成整数或者浮点数。我建议在代码层把它统一转成字符串,再用字符串方式去Assert,这样能规避很多类型不一致带来的奇怪失败。
4. 核心实现:读取Excel、循环执行、动态断言
4.1 用Python关键字把Excel读出来
我不太喜欢在Robot Framework里从头写一遍读取Excel的逻辑,那样代码会显得很啰嗦。更稳妥的做法是用Python封装一个读取函数,返回Robot可以直接遍历的list of dict结构。这样Robot那边只需要关心“拿到用例之后怎么发请求”,不用关心Excel的格式细节。
在resources/ExcelDataReader.py里,我写了这样的核心函数:
from openpyxl import load_workbook def read_excel_cases(file_path, sheet_name="接口用例"): wb = load_workbook(file_path, data_only=True) ws = wb[sheet_name] headers = [cell.value for cell in ws[1]] cases = [] for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2): if row[0] is None: continue raw = dict(zip(headers, row)) case = { "case_id": raw.get("用例编号"), "case_name": raw.get("用例名称"), "method": raw.get("请求方法"), "path": raw.get("请求路径"), "headers": raw.get("请求头"), "body": raw.get("请求体"), "expected_status": str(raw.get("预期状态码")), "json_path": raw.get("预期JSON路径"), "expected_value": raw.get("预期值"), "_row": row_idx, } enabled = raw.get("是否启用") if enabled == "是": cases.append(case) wb.close() return cases为什么用data_only=True?因为我需要读取单元格的计算结果,而不是公式本身。如果你的Excel里哪些字段用了公式,这个参数能保证你拿到的值是公式算出来的结果,而不是一串公式文本。
为什么要把行号记在case的“_row”字段里?因为后面执行完了要按原始行号把结果写回Excel。如果不记录,执行阶段就丢失了“这条用例来自第几行”的信息。
这个函数还会在读取时过滤掉“是否启用”不为“是”的行。这样Robot那边的FOR循环就简单很多,不需要每次判断用例要不要执行。
4.2 在Robot Framework里循环跑每条用例
接下来是核心的Robot套件设计。我在testcases/api_suite.robot里这样写:
*** Settings *** Library Collections Library RequestsLibrary Library ../resources/ExcelDataReader.py *** Variables *** ${BASE_URL} http://127.0.0.1:8080 ${CASE_FILE} ../testdata/api_cases.xlsx ${SHEET_NAME} 接口用例 *** Test Cases *** Run API Cases From Excel Create Session api ${BASE_URL} disable_warnings=1 ${cases}= read excel cases ${CASE_FILE} ${SHEET_NAME} FOR ${case} IN @{cases} ${result}= Run Single Case ${case} ENDCreate Session先建立一个可复用的HTTP会话,后面的请求都会带上这个会话的连接配置。这个会话同时也会管理Cookie,遇到需要登录态保持的接口会比较方便。disable_warnings=1是让Requests库不要打印那些关于HTTPS证书的告警,不然日志会非常嘈杂。
FOR循环是Robot Framework 4.0之后推荐的标准写法,语法简单直接。这里我不建议用旧版那种:FOR的写法,新项目直接上FOR就完了。
4.3 断言不止状态码:JSON路径校验
现在到了最关键的环节:怎么把一条Excel用例变成一次真实的接口调用,并且完成动态断言。
我在同一份Robot文件里定义了几个关键字。第一个关键字负责发请求:
*** Keywords *** Send Request By Case [Arguments] ${case} ${method}= Convert To Upper Case ${case}[method] ${headers}= Evaluate json.loads("""${case}[headers]""") if "${case}[headers]" != "" else {} modules=json ${body}= Evaluate json.loads("""${case}[body]""") if "${case}[body]" != "" else None modules=json ${resp}= POST On Session api ${case}[path] headers=${headers} json=${body} RETURN ${resp}这里我为了示例简洁,先固定用POST On Session来发请求。实际项目中,我需要根据用例里的method字段,分别调用GET On Session、PUT On Session、DELETE On Session。你也可以用Run Keyword去动态拼关键字名,但那样出错时的日志可读性会比较差。我个人的习惯是写一个完整的IF/ELSE分支,虽然代码略长,但排起错来非常直观。
第二个关键字负责跑单条用例的完整流程,包括发请求、状态码断言、字段断言、结果回写:
Run Single Case [Arguments] ${case} ${resp}= Send Request By Case ${case} Should Be Equal As Strings ${resp.status_code} ${case}[expected_status] ${json_path}= Get From Dictionary ${case} json_path ${expected_value}= Get From Dictionary ${case} expected_value IF "${json_path}" != "" ${json}= Set Variable ${resp.json()} ${actual_value}= get value by json path ${json} ${json_path} Should Be Equal As Strings ${actual_value} ${expected_value} END RETURN ${case}这里有一个小细节:状态码断言理应放在字段断言前面,因为字段断言依赖响应体是合法JSON,如果接口返回500或者超时,响应体可能不是JSON,先校验状态码能给出更明确的失败信息。
get value by json path需要Python库提供支持。我在ExcelDataReader.py里增加了一个简化版本的路径取值函数:
def get_value_by_json_path(json_data, path): if not path: return None current = json_data for key in path.split("."): if isinstance(current, dict): current = current.get(key) elif isinstance(current, list): try: current = current[int(key)] except (ValueError, IndexError): return None else: return None return current这个函数只支持点分路径,对付常见的嵌套JSON已经够用。如果团队里有更复杂的断言需求,比如需要判断数组长度、需要正则匹配、需要多级条件过滤,那可以升级成jsonpath-ng这类库来解析$..这种更灵活的表达式。最开始不需要一步到位,先跑通再增强是更现实的路线。
提示:在Robot Framework里对dict取值的写法很有讲究。${case}[method]这种写法能直接取到键对应的值,但如果键不存在,会直接报错。所以Robot调用前,我宁可在Python读取函数里把字段都补齐,也不要依赖Robot侧的容错。
5. 执行结果回写与日志排查
5.1 把结果直接写回Excel
接口跑完不能只把结果留在Robot的日志里,团队更习惯看Excel里的最后一列是PASS还是FAIL。所以我在ExcelDataReader.py里又加了一个回写函数:
def write_result_to_excel(file_path, sheet_name, row_number, actual_status, result, message=""): wb = load_workbook(file_path) ws = wb[sheet_name] ws.cell(row=row_number, column=12).value = actual_status ws.cell(row=row_number, column=13).value = result ws.cell(row=row_number, column=14).value = message wb.save(file_path) wb.close()每次只保存当前工作簿,这样在批量执行过程中不会积累文件句柄,避免Windows系统下常见的“文件被占用,无法写入”问题。如果用例数量特别大,每次都打开保存整个Excel文件确实有点慢,但对于几百条用例来说,这个耗时完全在可接受范围内。
在Robot那侧,Run Single Case里可以包一层异常处理。我习惯这样写:
Run Single Case [Arguments] ${case} TRY ${resp}= Send Request By Case ${case} Should Be Equal As Strings ${resp.status_code} ${case}[expected_status] ${json_path}= Get From Dictionary ${case} json_path ${expected_value}= Get From Dictionary ${case} expected_value IF "${json_path}" != "" ${json}= Set Variable ${resp.json()} ${actual_value}= get value by json path ${json} ${json_path} Should Be Equal As Strings ${actual_value} ${expected_value} END write result to excel ${CASE_FILE} ${SHEET_NAME} ${case}[_row] ${resp.status_code} PASS EXCEPT ${message} TYPE write result to excel ${CASE_FILE} ${SHEET_NAME} ${case}[_row] N/A FAIL ${message} Log ${message} level=ERROR ENDTRY/EXCEPT是Robot Framework 5.0引入的语法,如果你用的是旧版本Robot,需要换一种写法,比如Run Keyword And Expect Error配上自定义关键字,但可读性和排查体验都会差不少。所以我有意把示例建立在比较新的版本上,强烈建议团队升级到5.0以上。
一旦某条用例断言失败,异常被捕获后,错误信息会被写进Excel,同时Robot自己也会在log里打一条ERROR日志。这样你在Excel里看到FAIL,不用切换到log.html也能大概知道问题是什么。
5.2 结合log.html和report.html排查问题
Robot Framework自动生成的三个产物里,report.html是结果概览,log.html是详细的执行日志。排查失败用例的时候,我一般会先看Excel回写的错误信息,如果不够,再打开log.html搜索对应的用例编号。
log.html里会展示每一步关键字的入参和返回值,比如Send Request By Case收到的${case}字典、POST On Session返回的响应体、断言关键字比较的两个值。这个信息比单纯在Python里print要直观得多,也是我选择Robot FrameWork而不是自己写一套脚本的好处之一。
有个小技巧:在Robot关键字里,尽量把“请求路径”“请求体”“响应体”用Log关键字打出来。虽然RequestsLibrary本身会记录部分信息,但自己记录能保证格式完全可控。否则遇到某些接口返回极大响应体时,Robot日志会被刷得惨不忍睹,连正常内容都找不到。
6. 实战中踩过的坑和解决记录
6.1 Excel中文编码和单元格格式问题
Excel文件落地后,最常见的问题就是中文乱码和内容格式不一致。我自己就遇到过好几次这种事。
先说编码。用openpyxl读写xlsx格式,内部管理是Unicode,所以正常读写中文不会乱码。真正的坑在于:如果团队有人把Excel另存成了xls或者CSV文件,再放进测试数据目录,那代码读取时要么报格式不支持,要么就会出现编码解析问题。所以我的处理方式是明确约定:只支持xlsx格式,所有用例文件统一放在testdata目录。任何人提交文件之前,检查一下后缀。
第二个坑是单元格格式。Excel默认会把200这种数字当整数处理,把001这种字符串当数字处理成1。预期状态码被读成整数之后,在Robot里跟字符串比对就会失败。解决的办法是我前面提到的,在Python读取函数里统一转换成str。这一小行代码,省了我排查无数个类型不匹配的失败。
第三个坑更隐蔽:单元格里有不可见字符,比如Excel复制粘贴时带进来的换行符、制表符。这些字符不会在界面上直接显示,但进入Robot以后会导致断言值对比失败。我的排查方法是:写一个临时脚本,把读出来的case字段repr打印出来。一看就明白,原来值前后多了一个\n。后来我在读取函数里加了strip(),这个问题就彻底没了。
6.2 接口依赖与Token传递
接口自动化里最麻烦的不是发请求,而是接口之间的依赖。很多系统登录之后才能访问业务接口,Token如果每次手工填到Excel里,用例文件就失去了自动化意义。
我是这样处理的。首先在Excel里建一个配置sheet页,存放一些全局变量,比如登录接口的账号密码、Token的有效期、当前使用哪个环境。然后在Robot里加一个“登录并取Token”的关键字,执行入口先调用登录接口,把Token存成Suite级别的变量。后面所有用例的请求头里,都可以用${token}来引用。
实际做的时候大概长这样:
Login And Get Token Create Session auth ${BASE_URL} ${headers}= Create Dictionary Content-Type=application/json ${body}= Create Dictionary username=${ACCOUNT} password=${PASSWORD} ${resp}= POST On Session auth /api/login headers=${headers} json=${body} Should Be Equal As Strings ${resp.status_code} 200 ${token}= get value by json path ${resp.json()} data.token Set Suite Variable ${token}然后在Excel的请求头单元格里写:
{"Content-Type":"application/json", "Authorization":"Bearer ${token}"}Robot在Evaluate解析的时候,会先把带${token}的字符串替换成全局变量的实际值,再去解析JSON,所以这个方案是能直接跑的。这比在Excel里维护一个独立的Token列,再在代码里拼加请求头,要省事得多。
6.3 测试数据隔离和用例执行顺序
接口自动化踩过的另一个大坑,是测试数据之间互相污染。比如TC001创建了一个资源,TC002去查询这个资源,本意是一对依赖用例,但因为TC001在某个环境下执行失败,TC002的预期数据就没了,整个回归一片红。
我的建议是,每条接口用例尽量设计成独立可重复执行,不要依赖其他用例的执行结果。如果业务逻辑确实要求先创建再查询,那么你就把这套交互放在一条用例里,用连续性断言去覆盖,而不是拆成两条让它们隐式依赖。隐式依赖是非常危险的,它会让失败的分析变得极其复杂。
执行顺序上也有一点经验:Robot的FOR循环就是按照Excel的行顺序执行的。如果项目里暂时避免不了某些前后置依赖,那就确保把前置用例放在上面的行,不要在Excel里随意乱排。当然,更高级的做法是引入pabot做并行执行,但并行会带来更高的数据隔离要求,初期不建议并行。
7. 后续可以怎么扩展
这套Excel加Robot Framework的方案跑到稳定阶段之后,后面自然会有一些扩展方向。
第一个方向是环境切换。目前BASE_URL写死在Robot的Variables里,为了方便测试和预发环境来回切,可以把环境配置放到一个Python文件或者配置文件里,通过命令行参数传入。Robot Framework本身支持通过--variable参数覆盖变量,这样CI里跑不同环境的时候就不用来回改Excel了。
第二个方向是CI集成。把robot命令接到常见的CI流水线里,每次代码提交后自动跑接口回归,产出robot报告上传到CI平台。这个做法不复杂,效果却立竿见影,接口质量会从“靠人来盯”变成“由流水线守门”。
第三个方向是报告增强。Robot原生报告已经不错,但如果团队希望把结果汇总到自己的测试平台,可以解析output.xml,把每个用例的响应时间、状态码、断言结果提取出来,写入内部系统。很多测试平台都有提供解析Robot XML的现成依赖,不用自己从零写。
如果让我重新选一次,我可能还是会用Excel加Robot Framework来做接口自动化的第一版。不是说它完美,而是它足够简单,简单到团队里的每个人都能理解用例是怎么组织的,也足够灵活,灵活到能覆盖WebService接口测试的大部分核心场景。自动化用例不是越多越好,而是能被人持续维护、能在每次发布前真正减少手工回归工作量,才有价值。