api-testing
Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install scientiacapital-skills-api-testing-skill
Repository
Skill path: active/api-testing-skill
Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend, Testing, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: scientiacapital.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install api-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/scientiacapital/skills before adding api-testing to shared team environments
- Use api-testing for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: "api-testing"
description: "Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation."
---
<objective>
Expert-level skill for tool-based API testing using Postman and Bruno. Covers collection organization, environment management, test scripting, response validation, and CI/CD integration.
This skill complements testing-skill (code-based tests) and api-design-skill (API structure). Use this when you need to test existing APIs with dedicated tools rather than writing programmatic tests.
Key distinction:
- testing-skill: Code-based tests (supertest, MSW, pytest requests)
- api-testing-skill: Tool-based tests (Postman, Bruno collections)
- api-design-skill: How to design APIs (structure, conventions)
</objective>
<quick_start>
**Postman Quick Test:**
```javascript
// Tests tab in Postman
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response has user data", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json).to.have.property("email");
});
```
**Bruno Quick Test:**
```javascript
// tests/get-user.bru
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users/{{userId}}
}
tests {
test("should return 200", function() {
expect(res.status).to.equal(200);
});
}
```
**Environment Setup:**
```json
{
"baseUrl": "https://api.example.com",
"apiKey": "test_key_xxx"
}
```
</quick_start>
<success_criteria>
API testing is successful when:
- All endpoints have at least one happy path test
- Error cases tested (4xx, 5xx responses)
- Response schema validated (not just status codes)
- Environment variables used for all configurable values
- Collections organized by resource/domain
- Authentication flows tested end-to-end
- CI pipeline runs collections on every PR
- Test data is reproducible (fixtures or dynamic generation)
</success_criteria>
<tool_comparison>
## Postman vs Bruno
| Feature | Postman | Bruno |
|---------|---------|-------|
| Storage | Cloud/Local | Git-native (.bru files) |
| Collaboration | Team sync | Git branches |
| Pricing | Free tier + paid | Free and open source |
| Offline | Desktop app | Full offline |
| Scripting | JavaScript | JavaScript |
| CI/CD | Newman CLI | Bruno CLI |
| Schema | JSON | Plain text .bru |
| Best For | Teams, API documentation | Git workflows, privacy |
### When to Use Each
**Choose Postman when:**
- Team needs real-time collaboration
- API documentation is primary output
- Mock servers needed for frontend dev
- Complex OAuth flows with token refresh
**Choose Bruno when:**
- Git-native workflow preferred
- Privacy/self-hosting required
- Simpler test scenarios
- Developers prefer code-like syntax
</tool_comparison>
<collection_organization>
## Collection Structure
### Folder Hierarchy
```
my-api-tests/
├── auth/
│ ├── login.bru
│ ├── refresh-token.bru
│ └── logout.bru
├── users/
│ ├── create-user.bru
│ ├── get-user.bru
│ ├── update-user.bru
│ └── delete-user.bru
├── orders/
│ ├── create-order.bru
│ ├── get-orders.bru
│ └── cancel-order.bru
├── environments/
│ ├── local.bru
│ ├── staging.bru
│ └── production.bru
└── collection.bru
```
### Naming Conventions
| Element | Convention | Example |
|---------|------------|---------|
| Folders | kebab-case, plural | `users`, `auth-flows` |
| Requests | verb-noun | `create-user`, `get-orders` |
| Variables | camelCase | `{{baseUrl}}`, `{{authToken}}` |
| Environments | lowercase | `local`, `staging`, `production` |
### Request Ordering
Use sequence numbers for dependent requests:
```
1. auth/login.bru (seq: 1)
2. users/create-user.bru (seq: 2) - needs auth token
3. users/get-user.bru (seq: 3) - uses created user ID
```
</collection_organization>
<test_patterns>
## Test Assertion Patterns
### Status Code Validation
```javascript
// Postman
pm.test("Success response", () => pm.response.to.have.status(200));
pm.test("Created response", () => pm.response.to.have.status(201));
pm.test("Not found", () => pm.response.to.have.status(404));
// Bruno
test("Success response", () => expect(res.status).to.equal(200));
```
### Response Body Validation
```javascript
// Postman
pm.test("Has required fields", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json.id).to.be.a("string");
pm.expect(json.email).to.match(/^[\w-]+@[\w-]+\.\w+$/);
});
// Array validation
pm.test("Returns array of users", function () {
const json = pm.response.json();
pm.expect(json.users).to.be.an("array");
pm.expect(json.users.length).to.be.greaterThan(0);
});
```
### Response Time Validation
```javascript
pm.test("Response time < 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
```
### Header Validation
```javascript
pm.test("Content-Type is JSON", function () {
pm.response.to.have.header("Content-Type", /application\/json/);
});
pm.test("Has request ID", function () {
pm.response.to.have.header("X-Request-Id");
});
```
### JSON Schema Validation (Postman)
```javascript
const schema = {
type: "object",
required: ["id", "name", "email"],
properties: {
id: { type: "string", format: "uuid" },
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" }
}
};
pm.test("Schema is valid", function () {
pm.response.to.have.jsonSchema(schema);
});
```
</test_patterns>
<environment_management>
## Environment Management
### Variable Scopes (Postman)
```
Global → Collection → Environment → Data → Local
(lowest priority) (highest priority)
```
### Environment Files
**Postman environment.json:**
```json
{
"id": "env-uuid",
"name": "staging",
"values": [
{ "key": "baseUrl", "value": "https://staging.api.com", "enabled": true },
{ "key": "apiKey", "value": "stg_key_xxx", "enabled": true, "type": "secret" }
]
}
```
**Bruno environment:**
```javascript
// environments/staging.bru
vars {
baseUrl: https://staging.api.com
apiKey: stg_key_xxx
}
```
### Dynamic Variables
```javascript
// Pre-request script - set dynamic values
const timestamp = Date.now();
const uniqueEmail = `test_${timestamp}@example.com`;
// Postman
pm.environment.set("uniqueEmail", uniqueEmail);
pm.environment.set("timestamp", timestamp);
// Bruno (in pre-request)
bru.setVar("uniqueEmail", uniqueEmail);
```
### Chaining Requests
```javascript
// Request 1: Login - save token
pm.test("Save auth token", function () {
const json = pm.response.json();
pm.environment.set("authToken", json.accessToken);
pm.environment.set("userId", json.user.id);
});
// Request 2: Use saved token
// Headers: Authorization: Bearer {{authToken}}
// URL: {{baseUrl}}/users/{{userId}}
```
</environment_management>
<authentication_testing>
## Authentication Testing
### Bearer Token Flow
```javascript
// 1. Login request - Pre-request or separate request
// 2. Save token to environment
pm.environment.set("accessToken", pm.response.json().accessToken);
// 3. Use in subsequent requests
// Header: Authorization: Bearer {{accessToken}}
```
### API Key Authentication
```javascript
// Header-based
// X-API-Key: {{apiKey}}
// Query param
// GET {{baseUrl}}/endpoint?api_key={{apiKey}}
```
### OAuth 2.0 with Refresh
```javascript
// Pre-request script for token refresh
const tokenExpiry = pm.environment.get("tokenExpiry");
const now = Date.now();
if (!tokenExpiry || now > tokenExpiry) {
pm.sendRequest({
url: pm.environment.get("authUrl") + "/token",
method: "POST",
header: { "Content-Type": "application/x-www-form-urlencoded" },
body: {
mode: "urlencoded",
urlencoded: [
{ key: "grant_type", value: "refresh_token" },
{ key: "refresh_token", value: pm.environment.get("refreshToken") },
{ key: "client_id", value: pm.environment.get("clientId") }
]
}
}, (err, res) => {
if (!err) {
const json = res.json();
pm.environment.set("accessToken", json.access_token);
pm.environment.set("tokenExpiry", now + (json.expires_in * 1000));
}
});
}
```
### Testing Auth Failures
```javascript
// Test unauthorized access
pm.test("Returns 401 without token", function () {
pm.response.to.have.status(401);
});
// Test forbidden access
pm.test("Returns 403 for wrong role", function () {
pm.response.to.have.status(403);
pm.expect(pm.response.json().error).to.include("permission");
});
```
</authentication_testing>
<error_testing>
## Error Response Testing
### Validation Errors (400)
```javascript
pm.test("Returns validation error", function () {
pm.response.to.have.status(400);
const json = pm.response.json();
pm.expect(json.error).to.equal("VALIDATION_ERROR");
pm.expect(json.details).to.be.an("array");
pm.expect(json.details[0]).to.have.property("field");
pm.expect(json.details[0]).to.have.property("message");
});
```
### Not Found (404)
```javascript
pm.test("Returns 404 for missing resource", function () {
pm.response.to.have.status(404);
pm.expect(pm.response.json().error).to.equal("NOT_FOUND");
});
```
### Rate Limiting (429)
```javascript
pm.test("Rate limit headers present", function () {
pm.response.to.have.header("X-RateLimit-Limit");
pm.response.to.have.header("X-RateLimit-Remaining");
pm.response.to.have.header("X-RateLimit-Reset");
});
```
### Server Errors (5xx)
```javascript
// Test graceful error handling
pm.test("Error response has request ID", function () {
pm.expect(pm.response.json()).to.have.property("requestId");
});
```
</error_testing>
<ci_integration>
## CI/CD Integration
### Newman (Postman CLI)
```bash
# Install
npm install -g newman
# Run collection
newman run collection.json -e environment.json
# With reporters
newman run collection.json \
-e staging.json \
--reporters cli,junit \
--reporter-junit-export results.xml
# Specific folder
newman run collection.json --folder "users"
```
### Bruno CLI
```bash
# Install
npm install -g @usebruno/cli
# Run collection
bru run --env staging
# Specific file
bru run users/create-user.bru --env local
```
### GitHub Actions Example
```yaml
name: API Tests
on:
pull_request:
branches: [main]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Newman
run: npm install -g newman newman-reporter-htmlextra
- name: Run API Tests
run: |
newman run tests/api/collection.json \
-e tests/api/ci.json \
--reporters cli,htmlextra \
--reporter-htmlextra-export report.html
- name: Upload Report
uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-report
path: report.html
```
### Environment Secrets in CI
```yaml
# Use GitHub Secrets for sensitive values
- name: Run API Tests
env:
API_KEY: ${{ secrets.STAGING_API_KEY }}
run: |
newman run collection.json \
--env-var "apiKey=$API_KEY" \
-e staging.json
```
</ci_integration>
<data_management>
## Test Data Management
### Data Files (Newman)
```json
// test-data.json
[
{ "email": "[email protected]", "name": "User One" },
{ "email": "[email protected]", "name": "User Two" },
{ "email": "[email protected]", "name": "User Three" }
]
```
```bash
# Run with data iterations
newman run collection.json -d test-data.json -n 3
```
### Dynamic Data Generation
```javascript
// Pre-request script
const faker = require("faker"); // Postman has built-in faker
pm.environment.set("randomEmail", pm.variables.replaceIn("{{$randomEmail}}"));
pm.environment.set("randomName", pm.variables.replaceIn("{{$randomFullName}}"));
pm.environment.set("randomUUID", pm.variables.replaceIn("{{$guid}}"));
```
### Built-in Dynamic Variables (Postman)
| Variable | Example Output |
|----------|----------------|
| `{{$guid}}` | `a8b2c3d4-e5f6-7890-abcd-ef1234567890` |
| `{{$timestamp}}` | `1612345678` |
| `{{$randomEmail}}` | `[email protected]` |
| `{{$randomInt}}` | `42` |
| `{{$randomFullName}}` | `John Smith` |
### Cleanup Scripts
```javascript
// Post-request script - cleanup created resources
if (pm.response.code === 201) {
const createdId = pm.response.json().id;
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/users/" + createdId,
method: "DELETE",
header: { "Authorization": "Bearer " + pm.environment.get("authToken") }
}, (err, res) => {
console.log("Cleanup: deleted user " + createdId);
});
}
```
</data_management>
<checklist>
## API Testing Checklist
Before creating collection:
- [ ] API documentation reviewed
- [ ] Authentication method identified
- [ ] Base URLs for all environments defined
- [ ] Test data strategy determined
For each endpoint:
- [ ] Happy path test (expected input, expected output)
- [ ] Required field validation (400 errors)
- [ ] Authentication test (401 without token)
- [ ] Authorization test (403 wrong permissions)
- [ ] Not found test (404 invalid ID)
- [ ] Response schema validated
- [ ] Response time asserted
Collection organization:
- [ ] Requests grouped by resource
- [ ] Sequence numbers for dependent requests
- [ ] Environments for local/staging/production
- [ ] Sensitive values marked as secrets
CI integration:
- [ ] Newman/Bruno CLI configured
- [ ] GitHub Actions workflow created
- [ ] Test reports uploaded as artifacts
- [ ] Secrets stored in CI environment
</checklist>
<references>
For detailed patterns, load the appropriate reference:
| Topic | Reference File | When to Load |
|-------|----------------|--------------|
| Postman advanced patterns | `reference/postman-patterns.md` | Collections, scripting, monitors |
| Bruno workflow | `reference/bruno-patterns.md` | .bru files, git integration |
| Test case design | `reference/test-design.md` | Coverage strategies, edge cases |
| Test data strategies | `reference/data-management.md` | Fixtures, dynamic data, cleanup |
| CI/CD pipelines | `reference/ci-integration.md` | Newman, GitHub Actions, reporting |
**To load:** Ask for the specific topic or check if context suggests it.
</references>
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### reference/postman-patterns.md
```markdown
# Postman Advanced Patterns
## Collection Structure
### Folder Organization
```
My API Collection/
├── Setup/
│ ├── Health Check
│ └── Get Auth Token
├── Users/
│ ├── CRUD/
│ │ ├── Create User
│ │ ├── Get User
│ │ ├── Update User
│ │ └── Delete User
│ └── Search/
│ └── Search Users
├── Orders/
│ ├── Create Order
│ ├── Get Order
│ └── Cancel Order
└── Cleanup/
└── Delete Test Data
```
### Collection Variables
```javascript
// Set at collection level
pm.collectionVariables.set("collectionId", "abc123");
// Access
const id = pm.collectionVariables.get("collectionId");
```
---
## Pre-request Scripts
### Token Refresh Pattern
```javascript
const tokenUrl = pm.environment.get("tokenUrl");
const refreshToken = pm.environment.get("refreshToken");
const tokenExpiry = pm.environment.get("tokenExpiry");
// Check if token needs refresh (1 minute buffer)
if (!tokenExpiry || Date.now() > (tokenExpiry - 60000)) {
pm.sendRequest({
url: tokenUrl,
method: 'POST',
header: {
'Content-Type': 'application/json'
},
body: {
mode: 'raw',
raw: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
}
}, (err, response) => {
if (err) {
console.error('Token refresh failed:', err);
return;
}
const json = response.json();
pm.environment.set("accessToken", json.access_token);
pm.environment.set("tokenExpiry", Date.now() + (json.expires_in * 1000));
if (json.refresh_token) {
pm.environment.set("refreshToken", json.refresh_token);
}
});
}
```
### Generate Unique Test Data
```javascript
// Using built-in dynamic variables
const uniqueId = pm.variables.replaceIn('{{$guid}}');
const timestamp = pm.variables.replaceIn('{{$timestamp}}');
const randomEmail = pm.variables.replaceIn('{{$randomEmail}}');
pm.environment.set("testUserId", uniqueId);
pm.environment.set("testEmail", `test_${timestamp}@example.com`);
// Using JavaScript
const randomString = Math.random().toString(36).substring(7);
pm.environment.set("testSlug", `test-${randomString}`);
```
### Conditional Execution
```javascript
// Skip request based on condition
const shouldSkip = pm.environment.get("skipAuthTests") === "true";
if (shouldSkip) {
pm.execution.skipRequest();
}
```
---
## Test Patterns
### Comprehensive Response Validation
```javascript
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response time is acceptable", function () {
pm.expect(pm.response.responseTime).to.be.below(2000);
});
pm.test("Response has correct content type", function () {
pm.response.to.have.header("Content-Type");
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});
pm.test("Response body structure is valid", function () {
const json = pm.response.json();
pm.expect(json).to.be.an("object");
pm.expect(json).to.have.property("data");
pm.expect(json).to.have.property("meta");
});
pm.test("Data array is not empty", function () {
const json = pm.response.json();
pm.expect(json.data).to.be.an("array").that.is.not.empty;
});
```
### JSON Schema Validation
```javascript
const userSchema = {
type: "object",
required: ["id", "email", "name", "createdAt"],
properties: {
id: {
type: "string",
pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
},
email: {
type: "string",
format: "email"
},
name: {
type: "string",
minLength: 1,
maxLength: 255
},
createdAt: {
type: "string",
format: "date-time"
},
metadata: {
type: "object",
additionalProperties: true
}
},
additionalProperties: false
};
pm.test("Response matches user schema", function () {
const json = pm.response.json();
pm.expect(tv4.validate(json, userSchema)).to.be.true;
});
```
### Chained Request Validation
```javascript
// In Create User request tests
pm.test("Save created user ID", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
// Save for next request
pm.environment.set("createdUserId", json.id);
pm.environment.set("createdUserEmail", json.email);
});
// In Get User request tests
pm.test("Retrieved user matches created user", function () {
const json = pm.response.json();
const expectedEmail = pm.environment.get("createdUserEmail");
pm.expect(json.email).to.equal(expectedEmail);
});
```
---
## Workflows
### Collection Runner Setup
```javascript
// Run specific folders in order
// 1. Setup (auth)
// 2. Create resources
// 3. Test CRUD operations
// 4. Cleanup
// In Setup/Get Auth Token - tests:
pm.test("Auth token saved", function () {
const json = pm.response.json();
pm.environment.set("authToken", json.token);
});
// In Cleanup request - tests:
pm.test("Cleanup successful", function () {
pm.environment.unset("createdUserId");
pm.environment.unset("testEmail");
});
```
### Error Recovery
```javascript
// Handle failed prerequisite
pm.test("Handle missing auth token", function () {
if (!pm.environment.get("authToken")) {
pm.execution.setNextRequest("Get Auth Token");
return;
}
// Continue with normal tests
});
```
---
## Monitors
### Health Check Monitor
```javascript
// Scheduled to run every 5 minutes
pm.test("API is healthy", function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json.status).to.equal("healthy");
});
pm.test("Response time SLA", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Alert on failure (configure in Postman UI)
```
### Performance Baseline
```javascript
pm.test("Establish performance baseline", function () {
const responseTime = pm.response.responseTime;
// Log for trend analysis
console.log(`Response time: ${responseTime}ms`);
// Alert if degradation
const baseline = 200; // ms
const threshold = 1.5; // 50% degradation
pm.expect(responseTime).to.be.below(baseline * threshold);
});
```
---
## Mock Servers
### Creating Mocks
1. Create collection with example responses
2. Enable mock server in Postman
3. Use mock URL for frontend development
### Example Response Setup
```javascript
// Save as example response for mock
{
"id": "mock-user-123",
"email": "[email protected]",
"name": "Mock User",
"createdAt": "2024-01-01T00:00:00Z"
}
```
### Dynamic Mock Responses
```javascript
// In mock server's example, use variables
{
"id": "{{$guid}}",
"email": "{{$randomEmail}}",
"createdAt": "{{$isoTimestamp}}"
}
```
---
## Best Practices
### Variables Naming
```
// Environment-specific
baseUrl, apiKey, authToken
// Test data
testUserId, testEmail, createdResourceId
// Configuration
timeout, retryCount, skipCleanup
```
### Secret Management
```json
// Mark sensitive variables
{
"key": "apiKey",
"value": "secret_xxx",
"type": "secret", // Masks in UI
"enabled": true
}
```
### Documentation in Tests
```javascript
pm.test("User creation returns 201 with user object", function () {
// Verify:
// 1. Status code indicates resource created
// 2. Response includes auto-generated ID
// 3. Email matches input (case-insensitive)
// 4. Timestamps are set
pm.response.to.have.status(201);
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json.email.toLowerCase())
.to.equal(pm.environment.get("testEmail").toLowerCase());
pm.expect(json).to.have.property("createdAt");
});
```
```
### reference/bruno-patterns.md
```markdown
# Bruno Patterns
Bruno is a Git-native API client. All requests are stored as `.bru` files that can be version controlled.
## File Structure
### Basic .bru File
```javascript
meta {
name: Get User
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users/{{userId}}
body: none
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
headers {
Accept: application/json
X-Request-Id: {{$guid}}
}
tests {
test("Status is 200", function() {
expect(res.status).to.equal(200);
});
test("Has user data", function() {
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('email');
});
}
```
### POST Request with Body
```javascript
meta {
name: Create User
type: http
seq: 2
}
post {
url: {{baseUrl}}/api/users
body: json
auth: bearer
}
auth:bearer {
token: {{accessToken}}
}
headers {
Content-Type: application/json
}
body:json {
{
"email": "{{testEmail}}",
"name": "Test User",
"role": "member"
}
}
script:pre-request {
const timestamp = Date.now();
bru.setVar("testEmail", `test_${timestamp}@example.com`);
}
tests {
test("User created", function() {
expect(res.status).to.equal(201);
});
test("Save user ID", function() {
bru.setVar("createdUserId", res.body.id);
});
}
```
---
## Project Structure
### Recommended Layout
```
my-api/
├── bruno.json # Collection config
├── environments/
│ ├── local.bru
│ ├── staging.bru
│ └── production.bru
├── auth/
│ ├── login.bru
│ ├── refresh-token.bru
│ └── logout.bru
├── users/
│ ├── create-user.bru
│ ├── get-user.bru
│ ├── update-user.bru
│ ├── delete-user.bru
│ └── search-users.bru
├── orders/
│ ├── create-order.bru
│ ├── get-orders.bru
│ └── cancel-order.bru
└── _data/
└── test-users.json
```
### bruno.json Configuration
```json
{
"version": "1",
"name": "My API Tests",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
```
---
## Environments
### Environment File (.bru)
```javascript
// environments/staging.bru
vars {
baseUrl: https://staging.api.example.com
apiVersion: v1
}
vars:secret [
accessToken,
apiKey
]
```
### Multiple Environments
```javascript
// environments/local.bru
vars {
baseUrl: http://localhost:3000
apiVersion: v1
debug: true
}
// environments/production.bru
vars {
baseUrl: https://api.example.com
apiVersion: v1
debug: false
}
```
### Using Environment Variables
```bash
# CLI with environment
bru run --env staging
# Override variables
bru run --env staging --env-var "baseUrl=https://custom.api.com"
```
---
## Scripting
### Pre-request Scripts
```javascript
script:pre-request {
// Generate unique test data
const uuid = require('uuid');
bru.setVar("testId", uuid.v4());
// Set timestamp
bru.setVar("timestamp", Date.now());
// Conditional logic
if (!bru.getVar("accessToken")) {
console.log("Warning: No access token set");
}
}
```
### Post-response Scripts
```javascript
script:post-response {
// Save response data
if (res.status === 200) {
bru.setVar("lastResponseId", res.body.id);
}
// Log for debugging
console.log(`Response time: ${res.responseTime}ms`);
}
```
### Test Scripts
```javascript
tests {
test("Status code is 200", function() {
expect(res.status).to.equal(200);
});
test("Response has required fields", function() {
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('email');
expect(res.body.email).to.be.a('string');
});
test("Array response", function() {
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.be.greaterThan(0);
});
test("Response time acceptable", function() {
expect(res.responseTime).to.be.below(1000);
});
}
```
---
## Authentication
### Bearer Token
```javascript
auth:bearer {
token: {{accessToken}}
}
```
### API Key (Header)
```javascript
headers {
X-API-Key: {{apiKey}}
}
```
### API Key (Query Param)
```javascript
get {
url: {{baseUrl}}/api/data?api_key={{apiKey}}
}
```
### Basic Auth
```javascript
auth:basic {
username: {{username}}
password: {{password}}
}
```
### OAuth 2.0 Flow
```javascript
// auth/get-token.bru
meta {
name: Get OAuth Token
type: http
seq: 1
}
post {
url: {{authUrl}}/oauth/token
body: formUrlEncoded
}
body:form-urlencoded {
grant_type: client_credentials
client_id: {{clientId}}
client_secret: {{clientSecret}}
scope: read write
}
tests {
test("Save access token", function() {
expect(res.status).to.equal(200);
bru.setVar("accessToken", res.body.access_token);
bru.setVar("tokenExpiry", Date.now() + (res.body.expires_in * 1000));
});
}
```
---
## Request Chaining
### Sequential Execution
```javascript
// 1-login.bru (seq: 1)
meta {
name: Login
seq: 1
}
// Saves accessToken
// 2-create-user.bru (seq: 2)
meta {
name: Create User
seq: 2
}
// Uses {{accessToken}}, saves {{createdUserId}}
// 3-get-user.bru (seq: 3)
meta {
name: Get User
seq: 3
}
// Uses {{createdUserId}}
```
### Variable Passing
```javascript
// First request - tests section
tests {
test("Save for next request", function() {
bru.setVar("orderId", res.body.id);
bru.setVar("orderTotal", res.body.total);
});
}
// Second request - URL
get {
url: {{baseUrl}}/api/orders/{{orderId}}/details
}
```
---
## CLI Usage
### Basic Commands
```bash
# Run all requests in collection
bru run
# Run with specific environment
bru run --env staging
# Run specific file
bru run users/create-user.bru
# Run specific folder
bru run users/
# Run with environment variable override
bru run --env staging --env-var "baseUrl=http://localhost:3000"
```
### CI/CD Integration
```bash
# Run and output results
bru run --env ci --output results.json
# Fail on first error
bru run --env ci --bail
# Run with timeout
bru run --env ci --timeout 30000
```
### Example GitHub Action
```yaml
name: API Tests
on: [push, pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Bruno CLI
run: npm install -g @usebruno/cli
- name: Run API Tests
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
bru run --env ci \
--env-var "apiKey=$API_KEY"
```
---
## Best Practices
### File Naming
```
# Good
create-user.bru
get-user-by-id.bru
search-users.bru
delete-user.bru
# Avoid
user1.bru
test.bru
new.bru
```
### Sequence Numbers
```javascript
// Use seq for dependent requests
meta { seq: 1 } // Setup/auth
meta { seq: 2 } // Create resources
meta { seq: 3 } // Read/verify
meta { seq: 10 } // Cleanup (high number = runs last)
```
### Git Workflow
```bash
# .gitignore for Bruno
environments/local.bru # Local overrides
*.local.bru # Local files
.bruno/ # Cache directory
```
### Secret Management
```javascript
// environments/staging.bru
// Public variables
vars {
baseUrl: https://staging.api.com
apiVersion: v1
}
// Secret variables (not committed to git)
vars:secret [
accessToken,
apiKey,
clientSecret
]
```
### Documentation in Files
```javascript
meta {
name: Create Order
type: http
seq: 3
}
docs {
Creates a new order for the authenticated user.
Prerequisites:
- Valid access token (run login.bru first)
- User must have "create_order" permission
Expected response: 201 Created with order object
}
post {
url: {{baseUrl}}/api/orders
body: json
}
```
```
### reference/test-design.md
```markdown
# API Test Design
## Test Coverage Strategy
### Coverage Pyramid for APIs
```
┌─────────────────────┐
│ E2E Workflows │ ~10% - Full user journeys
│ (Login → Action) │
├─────────────────────┤
│ Integration │ ~30% - Multi-endpoint flows
│ (Create → Get) │
├─────────────────────┤
│ Endpoint Tests │ ~60% - Individual endpoints
│ (CRUD per entity) │
└─────────────────────┘
```
### Test Types
| Type | What It Tests | Example |
|------|---------------|---------|
| Smoke | API is alive | `GET /health` returns 200 |
| Functional | Correct behavior | `POST /users` creates user |
| Contract | Response structure | Response matches schema |
| Performance | Speed | Response < 500ms |
| Security | Auth/AuthZ | 401 without token |
---
## Test Case Design
### Happy Path Tests
For each endpoint, test the expected successful flow:
```javascript
// POST /api/users - Happy path
pm.test("Creates user with valid data", function () {
pm.response.to.have.status(201);
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json.email).to.equal(pm.environment.get("testEmail"));
pm.expect(json.createdAt).to.exist;
});
```
### Error Path Tests
| Error Type | HTTP Status | Test Focus |
|------------|-------------|------------|
| Validation | 400 | Missing/invalid fields |
| Authentication | 401 | Missing/invalid token |
| Authorization | 403 | Wrong permissions |
| Not Found | 404 | Invalid resource ID |
| Conflict | 409 | Duplicate creation |
| Rate Limit | 429 | Too many requests |
| Server Error | 5xx | Graceful error handling |
### Boundary Tests
```javascript
// Test string length limits
pm.test("Name max length enforced", function () {
// Request body has 256 char name (limit is 255)
pm.response.to.have.status(400);
pm.expect(pm.response.json().details[0].field).to.equal("name");
});
// Test numeric limits
pm.test("Quantity must be positive", function () {
// Request body has quantity: 0
pm.response.to.have.status(400);
});
// Test array limits
pm.test("Max 100 items per request", function () {
// Request body has 101 items
pm.response.to.have.status(400);
});
```
---
## CRUD Test Matrix
### Standard Tests per Endpoint
| Operation | Method | Tests |
|-----------|--------|-------|
| Create | POST | Valid data → 201, Invalid → 400, Duplicate → 409, Unauthorized → 401 |
| Read | GET | Exists → 200, Not found → 404, Unauthorized → 401 |
| Update | PUT/PATCH | Valid → 200, Invalid → 400, Not found → 404, Unauthorized → 401 |
| Delete | DELETE | Exists → 204, Not found → 404, Unauthorized → 401 |
| List | GET | Empty → 200 (empty array), With data → 200 (array), Pagination works |
### Example: User CRUD Tests
```
users/
├── create-user-happy.bru # 201 with valid data
├── create-user-invalid.bru # 400 missing email
├── create-user-duplicate.bru # 409 email exists
├── get-user-happy.bru # 200 existing user
├── get-user-not-found.bru # 404 invalid ID
├── update-user-happy.bru # 200 with valid changes
├── update-user-invalid.bru # 400 invalid email format
├── delete-user-happy.bru # 204 existing user
└── delete-user-not-found.bru # 404 invalid ID
```
---
## Response Validation
### Status Code Assertions
```javascript
// Success codes
pm.response.to.have.status(200); // OK
pm.response.to.have.status(201); // Created
pm.response.to.have.status(204); // No Content
// Client errors
pm.response.to.have.status(400); // Bad Request
pm.response.to.have.status(401); // Unauthorized
pm.response.to.have.status(403); // Forbidden
pm.response.to.have.status(404); // Not Found
pm.response.to.have.status(409); // Conflict
pm.response.to.have.status(422); // Unprocessable Entity
pm.response.to.have.status(429); // Too Many Requests
```
### Body Structure Assertions
```javascript
// Object structure
pm.test("Response has expected structure", function () {
const json = pm.response.json();
// Required fields
pm.expect(json).to.have.all.keys("id", "email", "name", "createdAt");
// Field types
pm.expect(json.id).to.be.a("string");
pm.expect(json.email).to.be.a("string");
pm.expect(json.metadata).to.be.an("object");
// Field formats
pm.expect(json.id).to.match(/^[a-f0-9-]{36}$/); // UUID
pm.expect(json.email).to.match(/^[\w-]+@[\w-]+\.\w+$/);
});
// Array structure
pm.test("List response structure", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("data");
pm.expect(json.data).to.be.an("array");
pm.expect(json).to.have.property("meta");
pm.expect(json.meta).to.have.property("total");
pm.expect(json.meta).to.have.property("page");
});
```
### Error Response Validation
```javascript
pm.test("Error response format", function () {
const json = pm.response.json();
// Standard error structure
pm.expect(json).to.have.property("error");
pm.expect(json).to.have.property("message");
pm.expect(json).to.have.property("requestId");
// Validation error specifics
if (pm.response.code === 400) {
pm.expect(json).to.have.property("details");
pm.expect(json.details).to.be.an("array");
json.details.forEach(detail => {
pm.expect(detail).to.have.property("field");
pm.expect(detail).to.have.property("message");
});
}
});
```
---
## Edge Cases
### Empty/Null Values
```javascript
// Test empty string
{ "name": "" } // Should fail validation
// Test null
{ "name": null } // Should fail or be ignored
// Test whitespace only
{ "name": " " } // Should fail validation
```
### Special Characters
```javascript
// Test Unicode
{ "name": "用户名" } // Chinese characters
// Test emoji
{ "name": "Test 🎉" } // Emoji in string
// Test injection attempts
{ "name": "'; DROP TABLE users; --" } // SQL injection
{ "name": "<script>alert(1)</script>" } // XSS
```
### Large Payloads
```javascript
// Test large strings
{ "description": "x".repeat(100000) } // 100KB string
// Test many array items
{ "tags": Array(1000).fill("tag") } // 1000 items
// Test deeply nested objects
{ "data": { "level1": { "level2": { ... } } } }
```
---
## Security Testing
### Authentication Tests
```javascript
// No token
pm.test("401 without token", function () {
// Remove Authorization header
pm.response.to.have.status(401);
});
// Invalid token
pm.test("401 with invalid token", function () {
// Authorization: Bearer invalid_token_xxx
pm.response.to.have.status(401);
});
// Expired token
pm.test("401 with expired token", function () {
// Use pre-expired token
pm.response.to.have.status(401);
});
```
### Authorization Tests
```javascript
// Wrong user's resource
pm.test("403 accessing other user's data", function () {
// User A trying to access User B's resource
pm.response.to.have.status(403);
});
// Wrong role
pm.test("403 without admin role", function () {
// Regular user accessing admin endpoint
pm.response.to.have.status(403);
});
```
### Input Validation Security
```javascript
// SQL injection attempt
pm.test("SQL injection blocked", function () {
// Body: { "search": "'; DROP TABLE users; --" }
pm.response.to.not.have.status(500);
// Should either 400 or process safely
});
// XSS attempt
pm.test("XSS sanitized", function () {
// Body: { "name": "<script>alert(1)</script>" }
const json = pm.response.json();
pm.expect(json.name).to.not.include("<script>");
});
```
---
## Performance Testing
### Response Time Thresholds
```javascript
// Individual endpoint
pm.test("Response under 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Tiered thresholds
pm.test("Response time acceptable", function () {
const endpoint = pm.request.url.path.join("/");
const thresholds = {
"/health": 100,
"/api/users": 500,
"/api/reports": 2000
};
const threshold = thresholds[endpoint] || 1000;
pm.expect(pm.response.responseTime).to.be.below(threshold);
});
```
### Load Testing Patterns
```javascript
// Data file iteration for load testing
// Run with: newman run collection.json -d users.json -n 100
// users.json
[
{ "email": "[email protected]" },
{ "email": "[email protected]" },
// ... 100 users
]
```
---
## Test Naming Conventions
### Good Test Names
```javascript
// Pattern: [method] [endpoint] - [scenario] → [expected result]
pm.test("POST /users - valid data → 201 with user object", ...);
pm.test("POST /users - missing email → 400 validation error", ...);
pm.test("GET /users/:id - valid ID → 200 with user data", ...);
pm.test("GET /users/:id - invalid ID → 404 not found", ...);
pm.test("DELETE /users/:id - no auth → 401 unauthorized", ...);
```
### Folder/File Naming
```
users/
├── post-create-user-success.bru
├── post-create-user-invalid-email.bru
├── post-create-user-duplicate.bru
├── get-user-success.bru
├── get-user-not-found.bru
└── delete-user-success.bru
```
```
### reference/data-management.md
```markdown
# Test Data Management
## Data Strategies
### Strategy Selection
| Strategy | When to Use | Pros | Cons |
|----------|-------------|------|------|
| Static fixtures | Deterministic tests | Reproducible | Stale data risk |
| Dynamic generation | Uniqueness needed | Fresh data | Non-deterministic |
| API seeding | Complex relationships | Realistic | Slower setup |
| Database snapshots | Integration tests | Fast reset | Environment coupling |
---
## Static Fixtures
### JSON Data Files
```json
// fixtures/users.json
[
{
"email": "[email protected]",
"name": "Test Admin",
"role": "admin"
},
{
"email": "[email protected]",
"name": "Test User",
"role": "member"
},
{
"email": "[email protected]",
"name": "Read Only User",
"role": "viewer"
}
]
```
### Using with Newman
```bash
# Run collection with data file
newman run collection.json -d fixtures/users.json
# Each iteration uses one data row
# Iteration 1: email = [email protected]
# Iteration 2: email = [email protected]
# Iteration 3: email = [email protected]
```
### Accessing Data Variables
```javascript
// In request body
{
"email": "{{email}}",
"name": "{{name}}",
"role": "{{role}}"
}
// In tests
pm.test("Created user matches input", function () {
const json = pm.response.json();
pm.expect(json.email).to.equal(pm.iterationData.get("email"));
});
```
---
## Dynamic Data Generation
### Postman Built-in Variables
```javascript
// UUID
{{$guid}} // "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
// Timestamp
{{$timestamp}} // "1612345678"
{{$isoTimestamp}} // "2024-01-15T10:30:00.000Z"
// Random data
{{$randomInt}} // 42
{{$randomEmail}} // "[email protected]"
{{$randomFullName}} // "John Smith"
{{$randomFirstName}} // "John"
{{$randomLastName}} // "Smith"
{{$randomPhoneNumber}} // "+1-555-123-4567"
{{$randomCity}} // "New York"
{{$randomCountry}} // "United States"
// Random strings
{{$randomAlphaNumeric}} // "a1b2c3"
{{$randomWords}} // "lorem ipsum dolor"
```
### Pre-request Script Generation
```javascript
// Generate unique test data
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(7);
pm.environment.set("testEmail", `test_${timestamp}@example.com`);
pm.environment.set("testUsername", `user_${randomSuffix}`);
pm.environment.set("testOrderId", `ORD-${timestamp}`);
// Generate realistic data
const names = ["Alice", "Bob", "Charlie", "Diana", "Eve"];
const randomName = names[Math.floor(Math.random() * names.length)];
pm.environment.set("testName", randomName);
// Generate valid phone number
const areaCode = Math.floor(Math.random() * 900) + 100;
const exchange = Math.floor(Math.random() * 900) + 100;
const subscriber = Math.floor(Math.random() * 9000) + 1000;
pm.environment.set("testPhone", `+1${areaCode}${exchange}${subscriber}`);
```
### Bruno Dynamic Variables
```javascript
script:pre-request {
const uuid = require('uuid');
bru.setVar("testId", uuid.v4());
bru.setVar("testEmail", `test_${Date.now()}@example.com`);
bru.setVar("testTimestamp", new Date().toISOString());
}
```
---
## Data Seeding
### API-Based Seeding
```javascript
// seed/create-test-user.bru
meta {
name: Seed Test User
seq: 1
}
post {
url: {{baseUrl}}/api/admin/seed
body: json
}
body:json {
{
"users": [
{
"email": "[email protected]",
"password": "Test123!",
"role": "admin"
}
],
"organizations": [
{
"name": "Test Org",
"plan": "enterprise"
}
]
}
}
tests {
test("Seeding successful", function() {
expect(res.status).to.equal(200);
bru.setVar("seededUserId", res.body.users[0].id);
bru.setVar("seededOrgId", res.body.organizations[0].id);
});
}
```
### Setup Collection Pattern
```
setup/
├── 01-reset-database.bru # Clear test data
├── 02-create-admin.bru # Create admin user
├── 03-create-test-org.bru # Create organization
├── 04-create-test-users.bru # Create test users
└── 05-verify-setup.bru # Verify setup complete
```
---
## Environment-Specific Data
### Environment Variables
```javascript
// environments/local.bru
vars {
baseUrl: http://localhost:3000
adminEmail: admin@localhost
testPassword: localtest123
}
// environments/staging.bru
vars {
baseUrl: https://staging.api.com
adminEmail: [email protected]
testPassword: stagingtest123
}
// environments/ci.bru
vars {
baseUrl: http://api:3000
adminEmail: [email protected]
testPassword: citest123
}
```
### Secret Management
```javascript
// Don't commit secrets - use CI environment variables
// environments/ci.bru
vars {
baseUrl: http://api:3000
}
vars:secret [
apiKey,
adminPassword,
testUserToken
]
```
```bash
# Set secrets via CLI
bru run --env ci \
--env-var "apiKey=$API_KEY" \
--env-var "adminPassword=$ADMIN_PASSWORD"
```
---
## Data Cleanup
### Post-Test Cleanup
```javascript
// In test's post-response script
script:post-response {
// Cleanup created resource
if (res.status === 201 && res.body.id) {
const deleteUrl = `${bru.getVar('baseUrl')}/api/users/${res.body.id}`;
// Mark for cleanup (actual deletion in cleanup request)
const toDelete = bru.getVar('resourcesToDelete') || [];
toDelete.push({ type: 'user', id: res.body.id });
bru.setVar('resourcesToDelete', JSON.stringify(toDelete));
}
}
```
### Cleanup Collection
```javascript
// cleanup/delete-test-resources.bru
meta {
name: Delete Test Resources
seq: 999 // Run last
}
script:pre-request {
const resources = JSON.parse(bru.getVar('resourcesToDelete') || '[]');
bru.setVar('cleanupResources', JSON.stringify(resources));
}
// Multiple DELETE requests in loop (Postman)
const resources = JSON.parse(pm.environment.get('resourcesToDelete') || '[]');
resources.forEach(resource => {
pm.sendRequest({
url: `${pm.environment.get('baseUrl')}/api/${resource.type}s/${resource.id}`,
method: 'DELETE',
header: {
'Authorization': `Bearer ${pm.environment.get('authToken')}`
}
}, (err, res) => {
if (err) {
console.log(`Failed to delete ${resource.type} ${resource.id}:`, err);
} else {
console.log(`Deleted ${resource.type} ${resource.id}`);
}
});
});
```
### Database Reset (CI)
```yaml
# GitHub Actions
- name: Reset Test Database
run: |
docker exec postgres psql -U postgres -d test -c "TRUNCATE users, orders CASCADE;"
- name: Run API Tests
run: newman run collection.json -e ci.json
```
---
## Data Isolation
### Test User Prefixes
```javascript
// Use consistent prefix for test data
const TEST_PREFIX = "test_";
pm.environment.set("testEmail", `${TEST_PREFIX}${Date.now()}@example.com`);
pm.environment.set("testUsername", `${TEST_PREFIX}user_${Date.now()}`);
// Easy to identify and clean up
// DELETE FROM users WHERE email LIKE 'test_%'
```
### Dedicated Test Tenant
```javascript
// environments/ci.bru
vars {
tenantId: test-tenant-001
baseUrl: https://api.example.com/test-tenant-001
}
// All test data isolated to this tenant
```
### Request Tagging
```javascript
// Add header to identify test requests
headers {
X-Test-Request: true
X-Test-Run-Id: {{testRunId}}
}
// Server can log/track test requests separately
```
---
## Best Practices
### Data Independence
```javascript
// BAD - Tests depend on each other
// Test 1 creates user
// Test 2 expects user from Test 1
// GOOD - Each test creates its own data
script:pre-request {
// Create fresh test user for this test
bru.setVar("testEmail", `test_${Date.now()}@example.com`);
}
```
### Deterministic IDs for Debugging
```javascript
// Use predictable IDs in specific tests
const testCaseId = "TC001";
const timestamp = Date.now();
pm.environment.set("testEmail", `${testCaseId}_${timestamp}@example.com`);
// Easy to trace: "[email protected]"
```
### Data Validation Before Use
```javascript
// Verify environment setup
pm.test("Environment configured correctly", function () {
pm.expect(pm.environment.get("baseUrl")).to.exist;
pm.expect(pm.environment.get("apiKey")).to.exist;
pm.expect(pm.environment.get("testUserEmail")).to.exist;
});
```
### Idempotent Setup
```javascript
// Create-if-not-exists pattern
script:pre-request {
const existingUserId = bru.getVar('testUserId');
if (!existingUserId) {
// User will be created by this request
bru.setVar('shouldCreateUser', 'true');
} else {
// Skip creation, user already exists
bru.setVar('shouldCreateUser', 'false');
}
}
```
```
### reference/ci-integration.md
```markdown
# CI/CD Integration for API Testing
## Newman (Postman CLI)
### Installation
```bash
# Global install
npm install -g newman
# Project dependency
npm install --save-dev newman
# With reporters
npm install -g newman-reporter-htmlextra newman-reporter-junit
```
### Basic Usage
```bash
# Run collection
newman run collection.json
# With environment
newman run collection.json -e staging.json
# With data file
newman run collection.json -d test-data.json
# Multiple iterations
newman run collection.json -n 5
# Specific folder only
newman run collection.json --folder "Users"
# Delay between requests (ms)
newman run collection.json --delay-request 100
# Timeout settings
newman run collection.json --timeout-request 30000 --timeout-script 5000
```
### Reporters
```bash
# CLI output (default)
newman run collection.json
# JUnit for CI
newman run collection.json \
--reporters junit \
--reporter-junit-export results.xml
# HTML report
newman run collection.json \
--reporters htmlextra \
--reporter-htmlextra-export report.html
# Multiple reporters
newman run collection.json \
--reporters cli,junit,htmlextra \
--reporter-junit-export results.xml \
--reporter-htmlextra-export report.html
```
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All tests passed |
| 1 | Test failures |
| 2 | Invalid collection |
| 3 | Runtime error |
---
## Bruno CLI
### Installation
```bash
# Global install
npm install -g @usebruno/cli
# Via npx (no install)
npx @usebruno/cli run
```
### Basic Usage
```bash
# Run all requests
bru run
# With environment
bru run --env staging
# Specific file
bru run users/create-user.bru --env local
# Specific folder
bru run users/ --env staging
# Environment variable override
bru run --env staging --env-var "baseUrl=http://localhost:3000"
# Output results
bru run --env ci --output results.json
```
---
## GitHub Actions
### Basic Workflow
```yaml
name: API Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Newman
run: npm install -g newman newman-reporter-htmlextra
- name: Run API Tests
run: |
newman run tests/api/collection.json \
-e tests/api/ci.json \
--reporters cli,htmlextra \
--reporter-htmlextra-export report.html
- name: Upload Report
uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-report
path: report.html
retention-days: 7
```
### With Secrets
```yaml
- name: Run API Tests
env:
API_KEY: ${{ secrets.STAGING_API_KEY }}
AUTH_TOKEN: ${{ secrets.TEST_AUTH_TOKEN }}
run: |
newman run collection.json \
-e ci.json \
--env-var "apiKey=$API_KEY" \
--env-var "authToken=$AUTH_TOKEN"
```
### With Service Container
```yaml
jobs:
api-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
api:
image: my-api:test
env:
DATABASE_URL: postgres://test:test@postgres:5432/test
ports:
- 3000:3000
steps:
- uses: actions/checkout@v4
- name: Wait for API
run: |
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
- name: Run Tests
run: |
newman run collection.json \
--env-var "baseUrl=http://localhost:3000"
```
### Matrix Testing (Multiple Environments)
```yaml
jobs:
api-tests:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [staging, production-readonly]
steps:
- uses: actions/checkout@v4
- name: Run Tests - ${{ matrix.environment }}
run: |
newman run collection.json \
-e environments/${{ matrix.environment }}.json \
--reporters cli,junit \
--reporter-junit-export results-${{ matrix.environment }}.xml
```
---
## GitLab CI
### Basic Pipeline
```yaml
# .gitlab-ci.yml
stages:
- test
api-tests:
stage: test
image: node:20
before_script:
- npm install -g newman newman-reporter-junit
script:
- newman run collection.json
-e ci.json
--reporters cli,junit
--reporter-junit-export results.xml
artifacts:
when: always
reports:
junit: results.xml
paths:
- results.xml
expire_in: 1 week
```
### With Variables
```yaml
api-tests:
stage: test
variables:
API_KEY: $STAGING_API_KEY # From GitLab CI/CD settings
script:
- newman run collection.json
-e ci.json
--env-var "apiKey=$API_KEY"
```
---
## CircleCI
### Basic Config
```yaml
# .circleci/config.yml
version: 2.1
jobs:
api-tests:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run:
name: Install Newman
command: npm install -g newman
- run:
name: Run API Tests
command: |
newman run collection.json \
-e ci.json \
--reporters cli,junit \
--reporter-junit-export results.xml
- store_test_results:
path: results.xml
- store_artifacts:
path: results.xml
workflows:
test:
jobs:
- api-tests
```
---
## Jenkins
### Pipeline Script
```groovy
pipeline {
agent any
tools {
nodejs 'node-20'
}
stages {
stage('Setup') {
steps {
sh 'npm install -g newman newman-reporter-junit'
}
}
stage('API Tests') {
steps {
withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) {
sh '''
newman run collection.json \
-e ci.json \
--env-var "apiKey=$API_KEY" \
--reporters cli,junit \
--reporter-junit-export results.xml
'''
}
}
post {
always {
junit 'results.xml'
}
}
}
}
}
```
---
## Reporting
### HTML Report (htmlextra)
```bash
newman run collection.json \
--reporters htmlextra \
--reporter-htmlextra-export report.html \
--reporter-htmlextra-title "API Test Report" \
--reporter-htmlextra-browserTitle "API Tests" \
--reporter-htmlextra-showEnvironmentData \
--reporter-htmlextra-showGlobalData
```
### JUnit for CI Integration
```bash
newman run collection.json \
--reporters junit \
--reporter-junit-export results.xml
```
### Custom JSON Output
```bash
newman run collection.json \
--reporters json \
--reporter-json-export results.json
```
### Combining Reporters
```bash
newman run collection.json \
--reporters cli,junit,htmlextra \
--reporter-junit-export junit-results.xml \
--reporter-htmlextra-export detailed-report.html
```
---
## Best Practices
### Environment Configuration
```javascript
// ci.json - Minimal CI environment
{
"name": "CI",
"values": [
{
"key": "baseUrl",
"value": "http://localhost:3000",
"enabled": true
},
{
"key": "timeout",
"value": "30000",
"enabled": true
}
]
}
```
### Fail Fast
```bash
# Stop on first failure
newman run collection.json --bail
```
### Parallel Execution
```yaml
# GitHub Actions - parallel jobs
jobs:
api-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
folder: [users, orders, payments]
steps:
- name: Run ${{ matrix.folder }} tests
run: |
newman run collection.json \
--folder "${{ matrix.folder }}" \
-e ci.json
```
### Retry on Flaky Tests
```yaml
# GitHub Actions retry
- name: Run API Tests
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: newman run collection.json -e ci.json
```
### Scheduled Health Checks
```yaml
# Run API health checks every hour
name: API Health Check
on:
schedule:
- cron: '0 * * * *' # Every hour
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Health Checks
run: |
newman run collection.json \
--folder "Health Checks" \
-e production.json
```
### Notifications
```yaml
# Slack notification on failure
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "API Tests Failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "API tests failed on `${{ github.ref_name }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
```
```