Bruno API Testing
Create and run API test collections using Bruno — a Git-first, offline-only API client that stores collections as plain files.
Format Selection
Bruno supports two file formats. Determine which to use:
- YAML (OpenCollection) — Default since Bruno v3.1. Uses
.ymlfiles andopencollection.ymlroot. Preferred for new projects. - Bru (Legacy) — Uses
.brufiles andbruno.jsonroot. Use only for existing Bru-format collections.
Detect format by checking the collection root: opencollection.yml → YAML, bruno.json → Bru.
For YAML format syntax details, see references/yaml-syntax.md. For Bru format syntax details, see references/bru-syntax.md.
Workflow
1. Create Collection Structure
Create the directory layout with the collection root file, environments, and organized request folders.
YAML format:
my-api-tests/
├── opencollection.yml # REQUIRED: collection root
├── environments/
│ ├── Local.yml
│ ├── Staging.yml
│ └── Production.yml
├── Auth/
│ ├── folder.yml
│ └── Login.yml
└── Users/
├── folder.yml
├── Get Users.yml
├── Get User by ID.yml
└── Create User.yml
Minimal opencollection.yml:
opencollection: 1.0.0
info:
name: My API Tests
Bru format: Same structure but use bruno.json + .bru extensions. See references/bru-syntax.md.
2. Create Environment Files
YAML (environments/Local.yml):
variables:
- name: baseUrl
value: http://localhost:3000/api
- name: apiKey
value: ""
secret: true
Bru (environments/Local.bru):
vars {
baseUrl: http://localhost:3000/api
}
vars:secret [
apiKey
]
3. Write Request Files with Tests
YAML format — a complete request with tests:
info:
name: Get Users
type: http
seq: 1
http:
method: GET
url: "{{baseUrl}}/users"
headers:
- name: accept
value: application/json
- name: authorization
value: "Bearer {{authToken}}"
runtime:
assertions:
- expression: res.status
operator: eq
value: "200"
- expression: res.body
operator: isArray
scripts:
- type: tests
code: |-
test("returns 200", function() {
expect(res.status).to.equal(200);
});
test("returns array of users", function() {
expect(res.body).to.be.an('array');
expect(res.body).to.have.lengthOf.at.least(1);
});
test("each user has required fields", function() {
res.body.forEach(user => {
expect(user).to.have.property('id');
expect(user).to.have.property('email');
});
});
settings:
encodeUrl: true
Use assertions (declarative) for simple checks, tests scripts (Chai.js) for complex logic.
4. Chain Requests with Data Extraction
Extract data from one response and use it in subsequent requests:
YAML — Login request saving a token:
info:
name: Login
type: http
seq: 1
http:
method: POST
url: "{{baseUrl}}/auth/login"
body:
type: json
data: |-
{
"username": "{{username}}",
"password": "{{password}}"
}
auth:
type: none
runtime:
scripts:
- type: after-response
code: |-
bru.setEnvVar("authToken", res.body.access_token);
- type: tests
code: |-
test("login successful", function() {
expect(res.status).to.equal(200);
expect(res.body).to.have.property('access_token');
});
Then reference {{authToken}} in subsequent requests via Bearer {{authToken}}.
5. Run Tests with bru CLI
Install and run:
npm install -g @usebruno/cli
# Run entire collection
cd my-api-tests && bru run --env Local
# Run specific folder
bru run Auth --env Local
# Run with developer mode (for external packages, fs access)
bru run --env Local --sandbox=developer
# Filter by tags
bru run --tags=smoke --env Local
# Generate reports
bru run --env Local \
--reporter-html results.html \
--reporter-junit results.xml \
--reporter-json results.json
# Pass secrets via CLI
bru run --env Local --env-var API_KEY=secret123
# Parallel execution
bru run --env Local --parallel
# Data-driven testing
bru run --csv-file-path data.csv --env Local
v3.0.0 breaking change: Default is now Safe Mode. Use --sandbox=developer for developer mode features.
6. Set Up CI/CD
See references/ci-cd.md for complete GitHub Actions workflows, matrix testing, and reporting patterns.
Minimal GitHub Actions workflow:
name: API Tests
on: [push, pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g @usebruno/cli
- name: Run API Tests
working-directory: ./my-api-tests
env:
API_KEY: ${{ secrets.API_KEY }}
run: bru run --env CI --reporter-html results.html --reporter-junit results.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
./my-api-tests/results.html
./my-api-tests/results.xml
Critical: Always set working-directory to the collection root in CI/CD.
Testing Patterns
Assertions (Declarative) — Use for Simple Checks
runtime:
assertions:
- expression: res.status
operator: eq
value: "200"
- expression: res.body.success
operator: eq
value: "true"
- expression: res.body.data
operator: isJson
- expression: res.headers.content-type
operator: contains
value: application/json
Operators vary slightly by Bruno version and editor surface. Check Bruno's current Assertions docs for the exact operator names supported by your version when writing declarative assertions.
Tests (Chai.js) — Use for Complex Validation
runtime:
scripts:
- type: tests
code: |-
test("status and structure", function() {
expect(res.status).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body).to.have.all.keys('id', 'name', 'email');
});
test("validates email format", function() {
expect(res.body.email).to.match(/^[\w\-.]+@([\w-]+\.)+[\w-]{2,4}$/);
});
test("response time acceptable", function() {
expect(res.responseTime).to.be.below(2000);
});
test("pagination works", function() {
expect(res.body.data).to.be.an('array');
expect(res.body.meta.total).to.be.a('number');
expect(res.body.meta.page).to.equal(1);
});
For the complete JavaScript API (req, res, bru objects), see references/javascript-api.md.
Common Mistakes
- Missing
opencollection.yml(YAML) orbruno.json(Bru) at collection root - Using
meta:instead ofinfo:in YAML request files - Using script type
testinstead oftests(plural) - Putting request-level fields (
http:,method:) inopencollection.yml - Forgetting
working-directoryin CI/CD steps - Committing secrets — use
secret: truein env files + CI/CD secrets - Using
|-for body data is required in YAML to preserve JSON formatting - Missing
seqnumber ininfo:— controls execution order - Relying on
folder.ymlscript/auth inheritance in CLI — Bruno's Sandwich execution order (Collection → Folder → Request) forbefore-requestscripts may work in the Bruno GUI, but@usebruno/clidoes NOT reliably inheritfolder.ymlscripts or auth settings to individual test files. Always addbefore-requestscripts andauthblocks directly to each request file that needs them. Thefolder.ymlis useful for documentation and GUI users, but CLI-driven tests must be self-contained.
Script Execution Order
Bruno supports two script flows:
- Sandwich (default): Collection
before-request→ Folderbefore-request→ Requestbefore-request→ Request is sent → Requestafter-response→ Folderafter-response→ Collectionafter-response - Sequential: Collection
before-request→ Folderbefore-request→ Requestbefore-request→ Request is sent → Collectionafter-response→ Folderafter-response→ Requestafter-response
Request assertions and request tests run after the post-response scripts.
⚠️ CLI Inheritance Caveat
The Sandwich/Sequential script execution order described above applies to Bruno GUI only. When running tests with @usebruno/cli (the CLI runner used in CI/CD), folder-level before-request scripts and auth settings are NOT reliably inherited by individual request files.
Impact: If a folder.yml contains a before-request script (e.g., to skip requests when an environment variable is "false"), request files in that folder will NOT inherit this logic when run via CLI.
Workaround: Duplicate the before-request logic into each individual request file that needs it:
# In each request file that needs conditional skip:
runtime:
scripts:
- type: before-request
code: |-
const featureAvailable = bru.getEnvVar("featureAvailable");
if (featureAvailable === "false") {
bru.runner.skipRequest();
}
- type: tests
code: |-
test("returns 200", function() {
expect(res.status).to.equal(200);
});
Same applies to auth: Set http.auth in each request file, not just folder.yml.
Variable Precedence (Highest to Lowest)
- Runtime variables (
bru.setVar()) - Request variables
- Folder variables
- Collection variables
- Environment variables
Use bru.getGlobalEnvVar() for global environment values and bru.getProcessEnv() for OS process environment variables. They are not documented as part of the standard collection variable precedence chain.
References
- YAML Syntax — Complete OpenCollection YAML format for requests, bodies, auth, headers, params, environments, folders, collections
- Bru Syntax — Legacy
.brufile format reference - JavaScript API — Full
req,res,bruobject API with runner control, cookies, utilities - CI/CD Integration — GitHub Actions workflows, report generation, matrix testing, environment secrets