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
Print Debug Informationβ
script:post-response {
console.log('Request:', {
method: bru.getRequestMethod(),
url: bru.getRequestUrl(),
headers: bru.getRequestHeaders()
});
console.log('Response:', {
status: bru.getResponseStatus(),
body: bru.getResponseBody()
});
}