Bruno: Git 友好的离线 API 测试工具¶
为什么要学 Bruno¶
Postman 曾是 API 测试的标配工具,但它的发展方向越来越让开发者不满: - 需要登录账号才能使用完整功能 - 集合(Collection)存储在云端,有隐私顾虑 - 免费版功能不断缩减 - 协作功能强制收费
Bruno 是一个开源的 API 测试客户端,核心理念:你的 API 集合就是项目中的文件,用 Git 管理。
| 维度 | Postman | Bruno |
|---|---|---|
| 数据存储 | 云端 | 本地文件系统 |
| 版本控制 | 内置(付费) | Git 原生 |
| 离线使用 | 部分功能 | 完全离线 |
| 账号要求 | 必需 | 不需要 |
| 文件格式 | JSON (私有) | Bru 文本格式 |
| 协作方式 | Postman Cloud | Git 仓库 |
| 费用 | 免费版受限 | 完全免费开源 |
| 隐私 | 请求可能经过 Postman 云 | 100% 本地 |
核心概念¶
白话解释¶
Bruno 的工作方式非常直接: 1. 在你的项目中创建一个文件夹存放 API 定义 2. 每个 API 请求是一个 .bru 文件(纯文本,可读) 3. 环境变量存在 .bru 文件中 4. 整个文件夹用 Git 管理,和代码一起版本控制
这意味着: - API 定义和代码在同一个仓库中 - PR review 时可以同时 review API 变更 - 新成员 clone 仓库就能直接使用所有 API 集合
核心概念表¶
| 概念 | 说明 | Postman 等价物 |
|---|---|---|
| Collection | 一组 API 请求的集合(目录) | Collection |
| Request | 单个 API 请求(.bru 文件) | Request |
| Environment | 环境变量配置 | Environment |
| Script | 请求前/后执行的脚本 | Pre-request/Tests Script |
| Bru Language | Bruno 的请求描述语言 | 无(Postman 用 JSON) |
| Runner | 批量运行请求集合 | Collection Runner |
| Assert | 响应断言 | Tests |
| Vars | 请求间变量传递 | Variables |
Bru 文件格式¶
meta {
name: 获取用户列表
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users
body: none
auth: bearer
}
auth:bearer {
token: {{authToken}}
}
headers {
Content-Type: application/json
Accept: application/json
}
query {
page: 1
limit: 20
}
script:pre-request {
bru.setVar("timestamp", Date.now());
}
script:post-response {
const data = res.getBody();
bru.setVar("firstUserId", data.users[0].id);
}
assert {
res.status: eq 200
res.body.users: isArray
}
tests {
test("should return users", function() {
expect(res.status).to.equal(200);
expect(res.body.users).to.be.an('array');
});
}
安装配置¶
安装方式¶
macOS
Linux
# Snap
sudo snap install bruno
# AppImage
# 从 https://github.com/usebruno/bruno/releases 下载
# Arch Linux
yay -S bruno-bin
Windows
CLI 工具(用于 CI/CD)
创建第一个集合¶
- 打开 Bruno
- 点击 "Create Collection"
- 选择项目目录下的文件夹(如
api-tests/) - 开始添加请求
或者手动创建目录结构:
your-project/
├── src/
├── api-tests/ # Bruno 集合目录
│ ├── bruno.json # 集合配置
│ ├── environments/ # 环境配置
│ │ ├── dev.bru
│ │ └── prod.bru
│ ├── auth/ # 按模块组织
│ │ ├── login.bru
│ │ └── register.bru
│ └── users/
│ ├── list.bru
│ ├── get.bru
│ └── create.bru
├── .gitignore
└── package.json
配置 bruno.json¶
{
"version": "1",
"name": "My API Tests",
"type": "collection",
"ignore": ["node_modules", ".git"]
}
配置环境¶
# environments/dev.bru
vars {
baseUrl: http://localhost:3000
authToken: dev-token-12345
}
# environments/prod.bru
vars {
baseUrl: https://api.example.com
authToken: {{process.env.PROD_TOKEN}}
}
快速上手¶
发送第一个请求¶
在 Bruno GUI 中,点击 "New Request" 或手动创建文件:
# api-tests/health.bru
meta {
name: Health Check
type: http
seq: 1
}
get {
url: {{baseUrl}}/health
body: none
auth: none
}
assert {
res.status: eq 200
}
常见请求类型¶
GET 请求
meta {
name: 获取用户详情
type: http
}
get {
url: {{baseUrl}}/api/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{authToken}}
}
POST 请求(JSON Body)
meta {
name: 创建用户
type: http
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
auth:bearer {
token: {{authToken}}
}
body:json {
{
"name": "张三",
"email": "zhangsan@example.com",
"role": "admin"
}
}
PUT 请求(表单数据)
meta {
name: 更新头像
type: http
}
put {
url: {{baseUrl}}/api/users/{{userId}}/avatar
body: multipartForm
auth: bearer
}
auth:bearer {
token: {{authToken}}
}
body:multipart-form {
avatar: @file(./test-avatar.png)
description: 新头像
}
使用变量¶
# 在 URL 中使用变量
get {
url: {{baseUrl}}/api/users/{{userId}}
}
# 在 Header 中使用变量
headers {
Authorization: Bearer {{authToken}}
X-Request-Id: {{$guid}}
}
# 在 Body 中使用变量
body:json {
{
"name": "{{userName}}",
"timestamp": "{{$timestamp}}"
}
}
内置变量: | 变量 | 说明 | |------|------| | {{$guid}} | 生成 UUID | | {{$timestamp}} | 当前时间戳 | | {{$isoTimestamp}} | ISO 格式时间 | | {{$randomInt}} | 随机整数 |
进阶用法¶
请求脚本¶
Pre-request Script(请求前执行)
script:pre-request {
// 动态生成认证信息
const crypto = require('crypto');
const timestamp = Date.now().toString();
const signature = crypto
.createHmac('sha256', bru.getEnvVar('apiSecret'))
.update(timestamp)
.digest('hex');
bru.setVar("timestamp", timestamp);
bru.setVar("signature", signature);
}
Post-response Script(响应后执行)
script:post-response {
// 提取 token 供后续请求使用
if (res.status === 200) {
const body = res.getBody();
bru.setVar("authToken", body.token);
bru.setVar("userId", body.user.id);
}
}
断言(Assert)¶
assert {
res.status: eq 200
res.body.data: isArray
res.body.data.length: gt 0
res.body.message: eq "success"
res.responseTime: lt 2000
res.headers.content-type: contains application/json
}
断言操作符: | 操作符 | 说明 | |--------|------| | eq | 等于 | | neq | 不等于 | | gt | 大于 | | gte | 大于等于 | | lt | 小于 | | lte | 小于等于 | | contains | 包含 | | isNull | 为 null | | isArray | 是数组 | | isJson | 是 JSON |
测试脚本¶
tests {
test("状态码应该是 200", function() {
expect(res.status).to.equal(200);
});
test("返回用户列表", function() {
const body = res.getBody();
expect(body.users).to.be.an('array');
expect(body.users.length).to.be.greaterThan(0);
});
test("每个用户都有必要字段", function() {
const body = res.getBody();
body.users.forEach(user => {
expect(user).to.have.property('id');
expect(user).to.have.property('name');
expect(user).to.have.property('email');
});
});
test("响应时间在合理范围内", function() {
expect(res.responseTime).to.be.below(2000);
});
}
CLI 用于 CI/CD¶
# 安装 CLI
npm install -g @usebruno/cli
# 运行整个集合
bru run --env dev
# 运行特定目录
bru run api-tests/auth --env dev
# 运行单个请求
bru run api-tests/health.bru --env dev
# 输出 JUnit 格式(CI/CD 集成)
bru run --env dev --format junit --output results.xml
GitHub Actions 集成
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
api-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g @usebruno/cli
- run: bru run api-tests --env ci --format junit --output results.xml
- uses: dorny/test-reporter@v1
if: always()
with:
name: API Test Results
path: results.xml
reporter: java-junit
请求链(Chain Requests)¶
通过脚本和变量,可以串联多个请求:
# 1. login.bru — 先登录获取 token
script:post-response {
const body = res.getBody();
bru.setVar("authToken", body.access_token);
}
# 2. get-profile.bru — 用 token 获取用户信息
auth:bearer {
token: {{authToken}}
}
在 Collection Runner 中按顺序运行这些请求,变量会自动传递。
常见问题¶
Q1: 从 Postman 迁移到 Bruno 难吗?¶
Bruno 支持导入 Postman 集合: 1. 在 Postman 中导出集合为 JSON 2. 在 Bruno 中选择 "Import Collection" → "Postman Collection" 3. 选择导出的 JSON 文件
大部分请求、变量、脚本都能自动转换。
Q2: Bruno 支持 WebSocket/gRPC 吗?¶
Bruno 主要关注 HTTP/REST API。WebSocket 和 gRPC 支持有限或在开发中。如果这些是核心需求,可以考虑搭配其他工具使用。
Q3: 团队如何协作?¶
通过 Git: 1. Bruno 集合目录放在项目仓库中 2. .bru 文件是纯文本,Git diff 友好 3. PR review 可以同时审查 API 测试变更 4. 不同分支可以有不同的 API 测试版本
Q4: 敏感信息(Token/密码)怎么处理?¶
# .gitignore 中排除包含敏感信息的环境
api-tests/environments/local.bru
# 使用环境变量引用
# dev.bru
vars {
apiToken: {{process.env.API_TOKEN}}
}
Q5: Bruno 和 Hurl 有什么区别?¶
- Bruno:有 GUI + CLI,适合日常开发和探索性测试
- Hurl:纯 CLI,纯文本格式,更适合 CI/CD 和自动化测试
- 两者可以共存:Bruno 用于开发时探索,Hurl 用于 CI/CD 验证
参考资源¶
| 资源 | 链接 |
|---|---|
| 官方网站 | https://www.usebruno.com |
| GitHub 仓库 | https://github.com/usebruno/bruno |
| 官方文档 | https://docs.usebruno.com |
| Bru 语言规范 | https://docs.usebruno.com/bru-language |
| CLI 文档 | https://docs.usebruno.com/cli |
| Postman 迁移指南 | https://docs.usebruno.com/importing/postman |
总结:Bruno 是 API 测试工具中"数据主权"意识最强的选择。如果你在意 API 定义的版本控制、团队协作、隐私保护,Bruno 的纯文件系统方案显著优于 Postman 的云端方案。它特别适合重视 Git 工作流的开发团队。