Skip to main content

Bruno API Collection Testing

Overview​

Bruno is a lightweight, open-source API testing tool for testing REST APIs with collections. The application uses Bruno to maintain shareable API test collections that can be version-controlled in git and run in CI/CD pipelines.

What is Bruno?​

Bruno provides:

  • Collection-Based Testing: Organize API tests into collections
  • Environment Variables: Dynamic values across requests
  • Pre-requests/Post-responses: Automation before/after requests
  • Assertions: Verify response data
  • Git-Versioned: Store collections in version control
  • CLI Testing: Run collections in CI/CD pipelines

QMS Bruno Setup​

Installation​

# Install Bruno CLI
pnpm add -D @usebruno/cli

# Or install globally
npm install -g @usebruno/cli

Directory Structure​

tests/api/
β”œβ”€β”€ qms/
β”‚ β”œβ”€β”€ bruno.json # Collection configuration
β”‚ β”œβ”€β”€ environments/
β”‚ β”‚ β”œβ”€β”€ dev.bru # Development environment
β”‚ β”‚ β”œβ”€β”€ staging.bru # Staging environment
β”‚ β”‚ └── production.bru # Production environment
β”‚ β”œβ”€β”€ auth/
β”‚ β”‚ β”œβ”€β”€ login.bru
β”‚ β”‚ β”œβ”€β”€ refresh-token.bru
β”‚ β”‚ └── logout.bru
β”‚ β”œβ”€β”€ employees/
β”‚ β”‚ β”œβ”€β”€ list-employees.bru
β”‚ β”‚ β”œβ”€β”€ create-employee.bru
β”‚ β”‚ β”œβ”€β”€ get-employee.bru
β”‚ β”‚ β”œβ”€β”€ update-employee.bru
β”‚ β”‚ └── delete-employee.bru
β”‚ β”œβ”€β”€ systems/
β”‚ β”‚ β”œβ”€β”€ list-systems.bru
β”‚ β”‚ └── create-system.bru
β”‚ └── shared/
β”‚ └── common-headers.bru

Creating Request Collections​

Environment Configuration​

environments/dev.bru​

auth:header:Authorization
~auth:secret
api_url: http://localhost:3001
api_version: v1

environments/staging.bru​

auth:header:Authorization
~auth:secret
api_url: https://api-staging.example.com
api_version: v1

Basic Request File​

auth/login.bru​

meta {
name: Login User
type: http
seq: 1
}

@baseUrl = {{api_url}}/api/{{api_version}}

post {{baseUrl}}/auth/login
{
"email": "user@example.com",
"password": "password123"
}

script:pre-request {
// Log the request
console.log(`Logging in as {{email}}`);
}

script:post-response {
// Extract token for later use
const response = bru.getResponseBody();
if (response.data && response.data.accessToken) {
bru.setEnvVar('auth_token', response.data.accessToken);
bru.setEnvVar('auth_secret', response.data.refreshToken);
}
}

assert {
res.status: 200
res.body.data.accessToken: truthy
res.body.data.refreshToken: truthy
}

Protected Endpoint Request​

employees/list-employees.bru​

meta {
name: List Employees
type: http
seq: 2
}

@baseUrl = {{api_url}}/api/{{api_version}}

get {{baseUrl}}/employees?page=1&limit=20
{
headers {
Authorization: Bearer {{auth_token}}
Content-Type: application/json
X-Request-ID: {{$randomUUID}}
}
}

query {
page: 1
limit: 20
department: Engineering
}

script:pre-request {
// Validate token exists
if (!bru.getEnvVar('auth_token')) {
throw new Error('Auth token not set. Run Login request first.');
}
}

script:post-response {
// Process response
const response = bru.getResponseBody();
bru.setEnvVar('employee_count', response.meta.total);

// Store first employee ID for next request
if (response.data.length > 0) {
bru.setEnvVar('first_employee_id', response.data[0].id);
}
}

assert {
res.status: 200
res.body.data: isArray
res.body.meta.page: 1
res.body.meta.total: greaterThan(0)
}

Create Resource Request​

employees/create-employee.bru​

meta {
name: Create Employee
type: http
seq: 3
}

@baseUrl = {{api_url}}/api/{{api_version}}

post {{baseUrl}}/employees
{
"name": "John Doe",
"email": "john.doe@example.com",
"department": "Engineering"
}
{
headers {
Authorization: Bearer {{auth_token}}
Content-Type: application/json
}
}

script:pre-request {
// Generate unique email
const timestamp = Date.now();
const uniqueEmail = `employee${timestamp}@example.com`;
bru.setEnvVar('employee_email', uniqueEmail);
}

script:post-response {
// Store created employee ID
const response = bru.getResponseBody();
if (response.data && response.data.id) {
bru.setEnvVar('created_employee_id', response.data.id);
console.log(`Employee created with ID: ${response.data.id}`);
}
}

assert {
res.status: 201
res.body.data.id: truthy
res.body.data.name: John Doe
res.body.data.email: {{employee_email}}
}

Update Resource Request​

employees/update-employee.bru​

meta {
name: Update Employee
type: http
seq: 4
}

@baseUrl = {{api_url}}/api/{{api_version}}

patch {{baseUrl}}/employees/{{created_employee_id}}
{
"department": "Management"
}
{
headers {
Authorization: Bearer {{auth_token}}
Content-Type: application/json
}
}

assert {
res.status: 200
res.body.data.department: Management
}

Delete Resource Request​

employees/delete-employee.bru​

meta {
name: Delete Employee
type: http
seq: 5
}

@baseUrl = {{api_url}}/api/{{api_version}}

delete {{baseUrl}}/employees/{{created_employee_id}}
{
headers {
Authorization: Bearer {{auth_token}}
}
}

assert {
res.status: 204
}

script:post-response {
// Verify employee is deleted
console.log('Employee deleted successfully');
bru.setEnvVar('created_employee_id', '');
}

Advanced Testing Features​

Parameterized Requests (Bulk Testing)​

employees/create-multiple-employees.bru​

meta {
name: Create Multiple Employees
type: http
seq: 6
}

@baseUrl = {{api_url}}/api/{{api_version}}

post {{baseUrl}}/employees
{
"name": "{{employee_name}}",
"email": "{{employee_email}}",
"department": "{{employee_department}}"
}
{
headers {
Authorization: Bearer {{auth_token}}
Content-Type: application/json
}
}

docs {
# Parameterized Request

Use this request with CSV data:

employee_name,employee_email,employee_department Alice Johnson,alice@example.com,Engineering Bob Smith,bob@example.com,Sales Carol Williams,carol@example.com,Marketing

}

Conditional Logic​

employees/get-or-create-employee.bru​

meta {
name: Get or Create Employee
type: http
seq: 7
}

@baseUrl = {{api_url}}/api/{{api_version}}

script:pre-request {
// Check if employee exists, if not create one
const employeeId = bru.getEnvVar('employee_id');

if (!employeeId) {
console.log('Employee ID not set, will create new employee');
} else {
console.log(`Using existing employee: ${employeeId}`);
}
}

get {{baseUrl}}/employees/{{employee_id}}
{
headers {
Authorization: Bearer {{auth_token}}
}
}

script:post-response {
const status = bru.getResponseStatus();

if (status === 404) {
console.log('Employee not found, creating new one');
// Trigger create request (in actual workflow)
} else if (status === 200) {
const response = bru.getResponseBody();
console.log(`Found employee: ${response.data.name}`);
}
}

assert {
res.status: 200
}

Data Validation​

employees/validate-employee-response.bru​

meta {
name: Validate Employee Response
type: http
seq: 8
}

@baseUrl = {{api_url}}/api/{{api_version}}

get {{baseUrl}}/employees/{{employee_id}}
{
headers {
Authorization: Bearer {{auth_token}}
}
}

script:post-response {
const response = bru.getResponseBody();
const employee = response.data;

// Validate structure
if (!employee.id || !employee.name || !employee.email) {
throw new Error('Invalid employee response structure');
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(employee.email)) {
throw new Error(`Invalid email format: ${employee.email}`);
}

// Validate department
const validDepartments = ['Engineering', 'Sales', 'Marketing', 'HR'];
if (!validDepartments.includes(employee.department)) {
throw new Error(`Invalid department: ${employee.department}`);
}

console.log('βœ“ Employee data validated successfully');
}

assert {
res.status: 200
res.body.data.id: isUUID
res.body.data.name: stringLength(1, 255)
res.body.data.email: isEmail
}

Response Assertion Methods​

# Equality
res.body.status: active
res.body.count: 5

# Truthiness
res.body.data: truthy
res.body.error: falsy

# Type checks
res.body: isObject
res.body.data: isArray
res.body.id: isString
res.body.count: isNumber
res.body.active: isBoolean

# String operations
res.body.name: contains(John)
res.body.email: startsWidth(user@)
res.body.url: regex(/^https:\/\//)

# Length/Size
res.body.data: isArray
res.body.name: stringLength(1, 100)

# Custom assertions
res.body.createdAt: isIsoDate
res.body.id: isUUID
res.body.email: isEmail

# Comparisons
res.body.count: greaterThan(0)
res.body.count: lessThan(100)
res.body.price: equalTo(99.99)

Running Tests from CLI​

Basic Commands​

# Run specific request
bruno run tests/api/qms --request "login.bru" \
--environment dev

# Run entire collection
bruno run tests/api/qms \
--environment dev \
--reporter json > results.json

# Run with specific outputs
bruno run tests/api/qms \
--environment staging \
--reporter html \
--output report.html

# Run in CI/CD mode
bruno run tests/api/qms \
--environment production \
--reporter junit \
--output test-results.xml

CI/CD Integration​

GitHub Actions Example​

name: API Tests (Bruno)

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: '20'

- run: npm install -g @usebruno/cli

- run: bruno run tests/api/qms --environment staging --reporter json --output results.json

- name: Upload results
uses: actions/upload-artifact@v3
with:
name: bruno-results
path: results.json

Collection Management​

Collection Configuration​

bruno.json​

{
"version": "1",
"name": "QMS API",
"uid": "qms-collection",
"root": ".",
"defaultEnvironment": "dev",
"environments": [
{
"name": "dev",
"path": "environments/dev.bru"
},
{
"name": "staging",
"path": "environments/staging.bru"
},
{
"name": "production",
"path": "environments/production.bru"
}
],
"preScripts": [
{
"name": "validate-environment",
"script": "if (!bru.getEnvVar('api_url')) throw new Error('api_url not set');"
}
]
}

Best Practices​

1. Organize by Resource/Feature​

tests/api/qms/
β”œβ”€β”€ auth/ # Authentication tests
β”œβ”€β”€ employees/ # Employee resource tests
β”œβ”€β”€ systems/ # System resource tests
└── shared/ # Shared utilities

2. Use Environment Variables for Configuration​

❌ AVOID: Hardcoded URLs
get http://staging.example.com/employees

βœ… GOOD: Use variables
get {{api_url}}/employees

3. Extract and Reuse Data​

script:post-response {
// Store for use in next request
bru.setEnvVar('user_id', response.data.id);
bru.setEnvVar('auth_token', response.data.token);
}

4. Add Documentation​

docs {
# List Employees Endpoint

Returns paginated list of employees.

## Query Parameters
- page: Page number (default: 1)
- limit: Results per page (default: 20)
- department: Filter by department

## Response
Returns 200 with employee array
}

5. Test Error Cases​

# Test invalid input
post {{baseUrl}}/employees
{
"name": "" # Invalid: empty name
}

assert {
res.status: 400
res.body.error.code: VALIDATION_ERROR
}

Common Variables​

# Built-in variables
{{$timestamp}} # Current timestamp
{{$randomUUID}} # Random UUID
{{$randomEmail}} # Random email
{{$randomInt}} # Random integer

# Environment variables
{{api_url}} # Base API URL
{{auth_token}} # Bearer token
{{employee_id}} # Extracted from previous response

Debugging​

Enable Verbose Logging​

bruno run tests/api/qms --environment dev --verbose
script:post-response {
console.log('Request:', {
method: bru.getRequestMethod(),
url: bru.getRequestUrl(),
headers: bru.getRequestHeaders()
});

console.log('Response:', {
status: bru.getResponseStatus(),
body: bru.getResponseBody()
});
}

Resources​