Bruno 替代 Postman:离线纯文本 API 测试与 Git 协作实践
2026/9/17 22:58:12 网站建设 项目流程

接口联调那几天,我又一次打开了 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),它是"这个环境长什么样",比如baseUrlapiVersion默认租户 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 环境,首次构建耗时
命令行工具(CLI)CI、批量执行可无头运行,输出报告与 GUI 版本要匹配

我自己的组合是:桌面端装安装包日常调接口,CI 里装 CLI 跑回归。两边读取的是同一套.bru文件,不存在"本地能跑 CI 跑不了"的问题。

有一点要提醒:GUI 和 CLI 的版本尽量对齐。我踩过一次坑,桌面端升级到了新版本,脚本里用了新 API,结果 CI 上的 CLI 还是旧版本,跑的时候直接报bru.setVar is not a function之外的一类奇怪错误。排查了半小时才想起来版本没同步。后来我在 CI 配置里把 CLI 版本号写死,升级时两边一起改。

3.2 新建集合与第一个请求

实操流程大致是这几步,我按真实顺序写:

  1. 打开工具,选择"打开集合"或"创建集合",指定一个本地目录。
  2. 在这个目录下新建文件夹,比如authorder,按业务域划分。
  3. 在文件夹上右键新建请求,填名称、方法、URL。
  4. 保存,此时磁盘上就多了一个.bru文件。
  5. 在集合根目录执行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.bru

vars { baseUrl: http://127.0.0.1:8080 apiVersion: v1 defaultTenant: tenant-local }

再建一个.env(不进仓库):

testUserPhone=13800000000 testUserPassword=your-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 是否过期需要刷新。

先说签名。很多内部网关要求请求头带上signtimestamp,算法通常是"参数按字典序拼接 + 密钥 + 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 done

5. 团队协作:把接口集合真正纳入 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未定义脚本语法体系不同逐条改写成对应的 API,pm.environment.get换成bru.getEnvVar
导入后环境变量为空环境是单独导入的环境文件要单独导入,导入后再逐一核对变量名
断言全挂断言语法差异pm.expectexpectpm.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,谁改了流程顺手改文档,慢慢就沉淀成了团队自己的规范。

再补一个体会:接口测试工具的选型,功能列表其实没那么重要,重要的是它能不能嵌进你现有的协作方式里。我最后留下来用的这套东西,赢的不是某个炫酷特性,而是让我不用再为"这份接口定义该怎么同步给同事"这种事分心。工具的存在感越低,说明它越贴合你的流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询