ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Bruno 替代 Postman:离线纯文本 API 测试与 Git 协作实践

Bruno 替代 Postman:离线纯文本 API 测试与 Git 协作实践 接口联调那几天我又一次打开了 Postman结果登录框先弹了出来——我只是想发一个 GET 请求看看返回体为什么得先连上账号。这大概是我第三次动迁移 API 测试工具的念头。过去两年我陆续试过几款替代品最后留在日常工具链里的是一款在社区里已经攒到 20K star 的开源工具Bruno。它吸引我的标签特别朴素完全离线、集合以纯文本保存、天生适配 Git 版本控制。这三个词拆开看都不新鲜但凑在一起正好把我在 API 调试和团队协作里最烦的那几个环节全解决了。这篇内容我打算把这套东西从选型思路到落地细节完整讲一遍包括我怎么把老项目的 Postman 集合迁过来、怎么和队友用 Git 管接口、以及踩过的那些不大不小但很恶心的坑。1. 为什么我要从 Postman 迁到纯文本的 API 测试工具1.1 Postman 让人不舒服的三件事先把话说清楚Postman 依然是这个品类里最成熟的产品功能覆盖面、文档生态、团队协作能力都很强我到现在也还留着它做个别场景的兜底。问题不在它好不好用而在于它的产品形态和我的使用场景之间有几处结构性的错位。第一件是登录与云同步的默认假设。新装一个客户端第一屏就是账号体系集合默认往云端走。这在内网环境或者客户现场是致命的机器不一定能出网就算能出接口地址、鉴权头、测试账号这些信息同步到外部服务合规上就要走审批。我遇到过最尴尬的一次是在客户机房里花二十分钟折腾账号同步最后发现不如直接开个 curl。第二件是集合的存储格式。Postman 导出的 collection 是一个体积很大的 JSON 文件所有请求、脚本、变量、示例塞在一个文件里。请求从 20 个涨到 200 个之后这个文件会膨胀到几千行甚至上万行。只要两个人同时改了两个不同的接口合并的时候就是一场灾难——整个文件会被判定为冲突你得盯着大段大段的 JSON 手工挑拣。第三件是协作能力的收费边界。基础的集合共享、环境变量同步、权限管理这些在团队规模稍微大一点之后就会碰到付费墙。这本身无可厚非商业产品要吃饭但如果团队只是想把接口定义和测试用例存进代码仓库跟着代码一起走为这点需求买一套协作席位账不太算得过来。1.2 纯文本集合到底解决了什么问题把集合从一个大 JSON换成一个请求一个文件听上去只是文件切分方式的改变实际上改变的是整个协作模型。你想想一个.bru文件里就一个接口定义几十行改了什么在git diff里一目了然。评审的时候同事能直接看到这个接口的 URL 从/api/v1/users改成了/api/v2/users多加了一个X-Trace-Id请求头而不是在一片 JSON 森林里找不同。举个我真实遇到的对比。老项目里有个下单接口同事改了请求体的字段名把goodsId改成skuId同时在 Postman 里顺手调了断言脚本。那次合并我花了差不多四十分钟因为整个 collection 文件被判冲突diff 出来八百多行。换成纯文本集合之后同样类型的改动冲突范围通常只落在一到两个文件里解决时间按分钟算。更关键的是接口集合和业务代码终于能放在同一个仓库、同一条时间线上。改后端接口的时候顺手改集合一起提交一起回滚。以前最难受的就是后端上了新版本前端还拿着两周前的集合调报 404 之后互相甩锅谁也说不出到底谁改的、什么时候改的。现在git log一翻清清楚楚。注意纯文本方案不是银弹。它把平台帮你管变成了你自己管如果团队里没人维护仓库规范文件会乱得比 JSON 更快。下面的协作章节我会专门讲规范怎么定。1.3 哪些人适合迁移哪些人先别急我一般不建议一上来就全量替换先按场景判断场景特征建议原因后端 / 全栈团队接口定义需要跟代码同步演进强烈建议迁移Git 工作流收益最大内网、客户现场、离线调试为主的场景强烈建议迁移离线优先是刚需有多环境dev/staging/prod且变量经常变建议迁移环境文件与代码分离切环境成本极低团队重度依赖 Postman 的 Mock Server、监控告警先观望或混用这部分能力替代品覆盖不完全完全非技术同学使用只要求点一下能跑先别急学习曲线虽平缓但仍有 Git 概念门槛需要复杂可视化报告、组织级权限体系谨慎评估企业级治理能力不是它的强项我自己的做法是双轨跑了一个月新接口全部用新工具写老接口按模块逐步迁迁完一个模块团队里确认没人回头用 Postman就把那部分从旧集合里删掉。一个月之后Postman 只剩两个历史遗留的复杂 Mock 场景。2. 核心设计思路拆解离线优先 Git 原生是怎么落地的2.1 一个.bru文件长什么样先看最直观的东西下面是一个带请求头、请求体、前置脚本和断言的真实例子meta { name: 创建订单 type: http seq: 3 } post { url: {{baseUrl}}/api/v2/orders body: json auth: bearer } auth:bearer { token: {{accessToken}} } headers { Content-Type: application/json X-Trace-Id: {{traceId}} } body:json { { skuId: SKU-10086, quantity: 2, channel: app } } script:pre-request { const ts Date.now().toString(); bru.setVar(traceId, trace-${ts}); console.log(traceId 已生成:, bru.getVar(traceId)); } tests { test(状态码应为 201, function() { expect(res.status).to.equal(201); }); test(返回体应包含订单号, function() { const data res.getBody(); expect(data.orderNo).to.be.a(string); expect(data.orderNo.length).to.be.greaterThan(0); }); }这个格式的妙处在于它是声明式的、块状的、可读的。不需要理解 JSON 嵌套层级人一眼就能看出这个请求用什么方法、打哪个地址、带什么头、跑什么脚本。语法本身很轻学起来大概十分钟。块与块之间是独立解析的这意味着两处不同位置的修改几乎不会互相干扰。我前面说的冲突范围收窄本质上就是这个文件结构带来的红利——头部改动落在headers块体改动落在body:json块脚本改动落在script块Git 的按行合并有很大概率能自动处理好。2.2 集合的目录结构与环境变量分离一个典型的集合文件夹大概是这样组织的order-service-api/ ├── bruno.json # 集合元信息 ├── environments/ │ ├── Local.bru # 本地环境变量 │ ├── Staging.bru │ └── Prod.bru ├── .env # 本地私有变量不进仓库 ├── auth/ │ ├── 登录.bru │ └── 刷新 Token.bru ├── order/ │ ├── 创建订单.bru │ ├── 查询订单详情.bru │ ├── 取消订单.bru │ └── 订单列表分页.bru └── common/ └── 健康检查.bru这个结构里有两层变量体系务必分清楚因为这是新手最容易搞混的地方。第一层是环境变量文件environments/*.bru它是这个环境长什么样比如baseUrl、apiVersion、默认租户 ID。这些东西团队共享、进仓库、可以评审。第二层是.env文件它是我这台机器上的私密信息比如真实的测试账号密码、个人 access token。这个文件要写进.gitignore永远不进仓库。环境变量文件内部大概是vars { baseUrl: https://staging.example.com apiVersion: v2 defaultTenant: tenant-demo }然后在请求里用{{baseUrl}}这种双花括号引用。切换环境就是把右上角的下拉从Local切到Staging所有请求的地址自动跟着变。这个体验和 Postman 的环境切换是一致的但好处是环境文件本身也是纯文本、也能 diff、也能回滚。我曾经因为有人误改了 staging 的地址导致一整个下午的联调全打到错误的机器上换成文本之后这种事故在 code review 阶段就被拦住了。2.3 为什么坚持不做云这个取舍这个工具最被讨论的设计决策就是它不提供云端同步。有人觉得这是缺陷我反而觉得这是它最清醒的地方。想想看一旦有了云产品就必须处理账号体系、权限模型、数据加密、合规审计、多租户隔离这一整套东西。成本飙升免费额度必然收紧最后又变成另一个需要买席位的平台。而不做云换来的是安装包干净、启动快、断网可用、数据边界清晰——你的接口信息压根不出你的机器和你的仓库。那团队之间怎么共享答案很土也很有效用 Git。仓库权限就是访问权限分支保护就是变更审批git log就是操作审计。这些都是团队已经在用的基础设施不需要再学一套。注意离线优先意味着没有自动备份。本地误删集合文件夹、又没提交过那就是真的没了。我现在的习惯是每天收工前git commit一次哪怕只改了一个字段。3. 从装到跑通第一个请求的完整实操3.1 安装方式对比与选择方式适用场景优点需要注意的点官方安装包dmg/exe/AppImage个人主力使用双击即用自动更新公司机器可能限制安装权限包管理器brew / scoop / snap开发机、需要版本统一一条命令搞定便于脚本化镜像源慢时要换源源码构建需要定制、内网无法拉包完全可控需要 Node 环境首次构建耗时命令行工具CLICI、批量执行可无头运行输出报告与 GUI 版本要匹配我自己的组合是桌面端装安装包日常调接口CI 里装 CLI 跑回归。两边读取的是同一套.bru文件不存在本地能跑 CI 跑不了的问题。有一点要提醒GUI 和 CLI 的版本尽量对齐。我踩过一次坑桌面端升级到了新版本脚本里用了新 API结果 CI 上的 CLI 还是旧版本跑的时候直接报bru.setVar is not a function之外的一类奇怪错误。排查了半小时才想起来版本没同步。后来我在 CI 配置里把 CLI 版本号写死升级时两边一起改。3.2 新建集合与第一个请求实操流程大致是这几步我按真实顺序写打开工具选择打开集合或创建集合指定一个本地目录。在这个目录下新建文件夹比如auth、order按业务域划分。在文件夹上右键新建请求填名称、方法、URL。保存此时磁盘上就多了一个.bru文件。在集合根目录执行git init写.gitignore首次提交。第三步有个小细节请求的seq序号建议手工维护。它决定请求在列表里的排列顺序也影响批量运行的默认顺序。我习惯把登录类请求排在最前面seq: 1因为后面的请求依赖登录拿到的 token。如果不管它新建的请求会随机插入批量跑的时候就会出现还没登录就去查订单的 401。bruno.json这个元信息文件也值得提一句它大概是这样{ version: 1, name: order-service-api, type: collection, ignore: [node_modules, .git] }团队统一这个文件里的name能让所有人的侧边栏显示一致评审截图的时候不会出现你那叫 order-api 我这叫 order-service的混乱。3.3 环境配置与变量替换的实操假设我要配一个本地环境。新建environments/Local.bruvars { baseUrl: http://127.0.0.1:8080 apiVersion: v1 defaultTenant: tenant-local }再建一个.env不进仓库testUserPhone13800000000 testUserPasswordyour-local-password accessToken然后在登录请求里这么写post { url: {{baseUrl}}/api/{{apiVersion}}/auth/login body: json } body:json { { phone: {{testUserPhone}}, password: {{testUserPassword}} } } tests { test(登录成功, function() { expect(res.status).to.equal(200); const token res.getBody().data.token; bru.setVar(accessToken, token); }); }注意最后一句bru.setVar(accessToken, token)把登录返回的 token 存进了运行时变量后续请求通过{{accessToken}}引用。这里的变量作用域要理清楚否则会出现这个请求能拿到、那个请求拿不到的鬼故事。大致的作用域优先级是这样的变量类型定义位置作用范围是否进仓库运行时变量setVar脚本里设置当前运行会话 / 集合否环境变量environments/*.bru该环境下的所有请求是本地私有变量.env本机所有环境否集合级变量集合设置里整个集合是我踩过最典型的坑是在 A 请求的pre-request脚本里setVar了一个值然后在 B 请求里用结果 A 没跑就直接跑 B值为空。运行时变量是有生命周期的它取决于执行顺序不是配置文件。所以我现在尽量把必须存在的值放进环境或.env只有运行时算出来的值才用setVar。注意.env一定要在.gitignore里。我见过有人把带真实测试账号的.env提交上去还振振有词说反正是测试环境。测试环境的账号往往也能登进后台风险等级没那么低。4. 进阶脚本、断言与命令行自动化4.1 前置脚本签名、时间戳与 Token 刷新前置脚本pre-request script跑在请求发出之前最常见的用途是三个生成时间戳、计算签名、判断 token 是否过期需要刷新。先说签名。很多内部网关要求请求头带上sign和timestamp算法通常是参数按字典序拼接 密钥 HMAC-SHA256。在脚本里实现大概是这样script:pre-request { const crypto require(crypto); const secret bru.getEnvVar(signSecret); const ts Math.floor(Date.now() / 1000).toString(); const raw appId${bru.getEnvVar(appId)}ts${ts}path${req.getUrl().getPath()}; const sign crypto.createHmac(sha256, secret).update(raw).digest(hex); req.setHeader(X-Timestamp, ts); req.setHeader(X-Sign, sign); }这里有个容易忽略的点签名用的路径必须是最终发出去的路径不能是你写在url里的带变量形式。如果 URL 里还带着{{apiVersion}}没被替换签出来的值和服务端算的对不上你会看到一堆 401 但完全不知道哪错了。调试的时候我习惯先console.log(raw)把参与签名的原始串打出来和网关同学的日志对一下通常两分钟就能定位。关于 token 刷新我的建议是不要在每个请求的前置脚本里都写一遍刷新逻辑。更干净的做法是建一个专门的登录/刷新请求在集合级别或文件夹级别配置运行前执行如果有该能力或者在 CI 脚本里先单独调一次登录。把刷新逻辑散落到每个请求里后面改一次认证方式你要改五十个文件。4.2 后置断言与测试用例组织后置断言tests 块用的是类 Chai 的语法expect(...).to.equal(...)这一套写起来的体验和后端单测很像上手没什么门槛。我的断言写法有个演进过程。刚开始只断言状态码tests { test(状态码 200, function() { expect(res.status).to.equal(200); }); }后来发现这远远不够。状态码 200 只说明网关通了业务可能返回code: 50001表示库存不足。于是加业务码断言tests { test(状态码 200, function() { expect(res.status).to.equal(200); }); test(业务码为 0, function() { const body res.getBody(); expect(body.code).to.equal(0); }); test(响应时间小于 800ms, function() { expect(res.getResponseTime()).to.be.lessThan(800); }); test(返回字段结构完整, function() { const data res.getBody().data; expect(data).to.have.property(orderNo); expect(data).to.have.property(status); expect(data.items).to.be.an(array); }); }第三个断言是我个人觉得收益最高的一个。响应时间断言能提前发现性能退化。不要求很严格给个宽松阈值比如 800ms 或者 1500ms一旦某次发版后这个接口慢了三倍回归测试会直接标红比等到线上告警要早得多。组织测试用例的时候我建议按冒烟 全量两层来分文件夹smoke/目录放核心链路五个以内请求每次提交都跑要求 30 秒内出结果。full/目录放全量回归包括边界值、异常分支每晚跑一次。这么分的好处是日常提交时 CI 不会被漫长的回归拖慢开发者愿意等真正需要覆盖的时候再跑全量。全都塞一起的结果通常是 CI 跑二十分钟然后大家都开始无视红叉。4.3 命令行运行与 CI 集成命令行工具大概是这个样子用# 全局安装 npm install -g usebruno/cli # 在集合目录下运行整个集合 bru run --env Local # 只跑冒烟目录 bru run smoke --env Local # 输出 JUnit 报告方便 CI 解析 bru run --env Local --reporter-junit results.xml # 临时覆盖某个变量常用于 CI 注入密钥 bru run --env Local --env-var accessToken$CI_TEST_TOKEN接进流水线的思路很直接在后端服务部署完成、健康检查通过之后加一个接口回归阶段执行 CLI 命令把results.xml交给 CI 的测试报告插件展示。失败就中断流程阻止前端或者下游服务基于坏接口继续联调。这里有两个实操细节值得说。第一变量注入优先用--env-var而不是把密钥写进环境文件。CI 的密钥管理通常有自己的机制用命令行参数注入既不会污染仓库也不会出现在日志里注意某些 CI 会回显命令必要时用管道或者临时文件传参。第二给 CLI 设超时和重试。我遇到过一次 CI 偶发失败原因是测试环境的网关在滚动重启请求超时。后来在命令外加了简单的重试逻辑把偶发失败和环境真故障区分开。判断方法是重试一次就过的是抖动三次都失败的是真问题。for i in 1 2 3; do bru run smoke --env Staging --reporter-junit results.xml break echo 第 $i 次失败10 秒后重试 sleep 10 done5. 团队协作把接口集合真正纳入 Git 工作流5.1.gitignore与文件边界仓库里什么该进、什么不该进最好在项目第一天就定死后面再补规矩的代价很高。我的模板大概是# 本地私有变量绝不允许提交 .env .env.* # 各个开发者自己的临时环境 environments/Local.bru environments/*.local.bru # 编辑器与依赖 .vscode/ .idea/ node_modules/ # 测试产物 results.xml reports/这里有争议的是environments/Local.bru到底要不要忽略。我的做法是忽略理由是每个人的本地端口、本地数据库账号都不一样进仓库只会制造无意义的冲突。每个新同学入职时从Local.example.bru复制一份改名就行# environments/Local.example.bru vars { baseUrl: http://127.0.0.1:8080 apiVersion: v1 defaultTenant: tenant-local }这个example文件进仓库起到说明书的作用。新人照着复制五分钟就能跑起来。同理.env.example也值得留一份把需要哪些变量列清楚但不填真实值# .env.example testUserPhone testUserPassword signSecret注意example文件里千万不要顺手填一个方便测试的真实账号。我见过不止一个仓库这么干结果就是密钥泄露的经典案例。5.2 冲突处理与 code review 实践即便文件被切得很细冲突依然会发生。最常见的是两个人同时改了同一个请求的headers块。处理方式其实和写代码一样看 diff、理解双方意图、手工合并headers { Content-Type: application/json HEAD X-Client-Version: 3.2.0 X-Device-Id: {{deviceId}} feature/device-tracking }这种纯文本冲突解决起来比 JSON 舒服太多——两个改动各占一行保留哪行、还是都要一眼就清楚。Code review 的时候我重点看四件事供你参考URL 和版本号有没有跟着后端改。v1写成v2这类低级错误评审阶段抓最便宜。断言有没有被顺手删掉。有人改接口发现断言失败第一反应是删断言而不是查原因这个必须拦。敏感值有没有硬编码。直接在文件里写死一个 token 或者手机号属于必须打回的改动。seq顺序有没有被破坏。尤其是新加了依赖 token 的请求却排在登录之前。我还习惯在 PR 描述里贴一张运行结果的截图。纯文本集合很容易做到这一点跑一遍bru run smoke截个全绿的图评审人心里就有底了。5.3 敏感数据与密钥管理的分层这块我总结成一个三层模型团队里推广之后效果不错层级存放位置内容示例进仓库公开层environments/*.brubaseUrl、apiVersion、租户标识是岗位共享层团队密钥管理系统测试账号、签名密钥否通过注入获取个人层本机.env个人调试 token、本地端口否拉开层次之后这个变量该放哪就不再需要每次讨论。公开层的东西随便改改了走评审中间层由运维统一管理通过 CI 注入或者本地拉取个人层的随便折腾反正不进仓库。有个细节容易被忽略日志里也会泄露密钥。前置脚本里如果console.log把 token 打出来了加上某些工具的运行历史功能这个 token 就留在了本地数据库里。我在共享屏幕演示之前一定会先清一遍运行历史并把脚本里的调试console.log注释掉。6. 常见问题与排查速查表6.1 从 Postman 迁移过程中的坑迁移不是一键完成的事哪怕有导入功能。下面这几个是我实际遇到过的现象原因处理办法导入后脚本报pm未定义脚本语法体系不同逐条改写成对应的 APIpm.environment.get换成bru.getEnvVar导入后环境变量为空环境是单独导入的环境文件要单独导入导入后再逐一核对变量名断言全挂断言语法差异pm.expect换expectpm.response.json()换res.getBody()请求顺序乱了seq没有正确生成手工调整序号把依赖型请求排在后面中文请求名乱码文件编码问题统一存成 UTF-8导入后检查一遍文件编码我建议按模块迁不要按文件迁。一个业务域的所有接口一次性迁完迁完就把该模块的冒烟用例跑一遍通过了再迁下一个。按文件迁容易迁到一半停下来两套工具并行维护最后两边都不同步。6.2 变量与脚本类的典型问题这类问题占了我在群里被问到的比例的一大半整理成速查表报错或现象可能原因排查动作请求里{{token}}原样发出去了变量未定义被当成字面量检查环境是否选中、变量名拼写、大小写拿到的是上一个环境的地址环境切换后没保存/没生效确认右上角环境名重启一次请求登录接口返回 200 但后续 401token 没写进变量或没被读取打印bru.getVar(accessToken)看是否有值脚本里的console.log没输出看错面板或脚本块名写错确认是script:pre-request还是script:post-response断言报res is not defined断言写在了前置脚本块断言必须放在后置的tests块数组取值报错响应结构比预期多包了一层先console.log(JSON.stringify(res.getBody()))看真实结构最后一条我想多说一句。很多断言失败其实不是接口坏了是你对响应结构的假设错了。我现在的习惯是写断言之前先不加任何断言裸跑一次把返回体完整打出来看清楚再写expect。这个习惯帮我省下大量无效排查时间。6.3 和其他工具链混用时的注意事项现实里很少有人能一次性换干净混用期有几点要注意。接口定义来源要唯一。如果 OpenAPI 文档、Postman 集合、新工具集合三份并存很快就会互相矛盾。我的做法是明确一个真源以代码仓库里的接口定义为真源工具集合作为消费方定期同步。其他来源一概作废。报告要汇总到一个地方看。CI 里可能有单元测试报告、接口回归报告、前端 E2E 报告最好都转成同一种格式比如 JUnit XML在同一个页面展示。否则开发者要开三个页面才知道自己这次提交到底挂在哪。别在两个工具里维护同一份断言。我见过团队在 Postman 里有一套断言在新工具里又抄了一套后来接口改了两套断言一个改了一个没改CI 一直是红的但没人知道该信哪个。断言的唯一归属地必须明确。逐步淘汰旧工具时留个冻结策略。老集合不删标个注释说明仅作历史参考不再维护避免有人误改。等到确认没人访问再删。最后分享一个我用了很久的小技巧。我会在集合根目录放一个README.md里面就三样东西怎么本地跑起来、变量从哪来、CI 命令是什么。每次新人问这个怎么用我直接甩链接比口头讲十遍都管用。这份 README 也跟接口集合一起进 Git谁改了流程顺手改文档慢慢就沉淀成了团队自己的规范。再补一个体会接口测试工具的选型功能列表其实没那么重要重要的是它能不能嵌进你现有的协作方式里。我最后留下来用的这套东西赢的不是某个炫酷特性而是让我不用再为这份接口定义该怎么同步给同事这种事分心。工具的存在感越低说明它越贴合你的流程。
返回列表