Comprehensive API testing guide covering Postman automation, k6 load testing, Pact contract testing, and API testing best practices for CI/CD pipelines.
API Testing: Complete Guide
Postman Collections and Automation
// Postman pre-request script
pm.environment.set("timestamp", Date.now())
pm.environment.set("request_id", pm.variables.replaceIn("{{$randomUUID}}"))
// HMAC authentication
const crypto = require('crypto-js')
const secret = pm.environment.get("API_SECRET")
const body = pm.request.body.raw || ''
const signature = crypto.HmacSHA256(body, secret).toString()
pm.request.headers.add({key: 'X-Signature', value: signature})
// Postman test script
pm.test("Status is 200", () => pm.response.to.have.status(200))
pm.test("Response time < 500ms", () => pm.expect(pm.response.responseTime).to.be.below(500))
pm.test("Response has required fields", () => {
const json = pm.response.json()
pm.expect(json).to.have.property('id')
pm.expect(json).to.have.property('email')
pm.expect(json.email).to.match(/^[^s@]+@[^s@]+.[^s@]+$/)
})
// Store response for use in subsequent requests
pm.test("Store user ID", () => {
const json = pm.response.json()
pm.environment.set("userId", json.id)
})
Newman CLI (Postman automation)
# Install
npm install -g newman
# Run collection
newman run MyAPI.postman_collection.json --environment prod.postman_environment.json --reporters cli,junit --reporter-junit-export results.xml
# With data file (data-driven testing)
newman run MyAPI.postman_collection.json --data test-data.json --environment staging.json
# GitHub Actions integration
# - uses: actions/checkout@v4
# - run: npm install -g newman
# - run: newman run tests/api.collection.json -e tests/staging.env.json
k6 Load Testing
// k6/load-test.js
import http from 'k6/http'
import { check, sleep, group } from 'k6'
import { Rate, Trend, Counter } from 'k6/metrics'
// Custom metrics
const errorRate = new Rate('errors')
const loginDuration = new Trend('login_duration')
const totalRequests = new Counter('total_requests')
export const options = {
stages: [
{ duration: '30s', target: 10 }, // Ramp up
{ duration: '1m', target: 50 }, // Stay at 50 users
{ duration: '30s', target: 100 }, // Spike
{ duration: '30s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95% under 500ms
errors: ['rate<0.01'], // < 1% errors
http_req_failed: ['rate<0.01'],
},
}
const BASE_URL = 'https://api.example.com'
export function setup() {
// Login and get token
const resp = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: 'test@example.com',
password: 'password123',
}), { headers: { 'Content-Type': 'application/json' } })
return { token: resp.json('access_token') }
}
export default function(data) {
const headers = {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
}
group('User Operations', () => {
// GET users
const listResp = http.get(`${BASE_URL}/users`, { headers })
check(listResp, {
'list status 200': (r) => r.status === 200,
'list has users': (r) => r.json('data').length > 0,
})
errorRate.add(listResp.status !== 200)
totalRequests.add(1)
sleep(0.5)
// Create user
const start = Date.now()
const createResp = http.post(`${BASE_URL}/users`,
JSON.stringify({ name: 'Test User', email: `test${Date.now()}@example.com` }),
{ headers }
)
loginDuration.add(Date.now() - start)
check(createResp, {
'create status 201': (r) => r.status === 201,
})
})
sleep(1)
}
export function teardown(data) {
// Cleanup after test
console.log('Test complete')
}
# Run k6 test
k6 run k6/load-test.js
# Run with cloud
k6 cloud k6/load-test.js
# Generate HTML report
k6 run --out json=results.json k6/load-test.js
k6 report results.json > report.html
Contract Testing with Pact
// Consumer test - defines contract
import { Pact } from '@pact-foundation/pact'
import { fetchUser } from '../src/api/userService'
const provider = new Pact({
consumer: 'frontend-app',
provider: 'user-api',
port: 8080,
dir: './pacts',
})
describe('User API Contract', () => {
beforeAll(() => provider.setup())
afterAll(() => provider.finalize())
it('should fetch user by ID', async () => {
await provider.addInteraction({
state: 'User 123 exists',
uponReceiving: 'a request for user 123',
withRequest: {
method: 'GET',
path: '/users/123',
headers: { Accept: 'application/json' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: '123',
name: like('John Doe'),
email: like('john@example.com'),
createdAt: like('2026-01-01T00:00:00Z'),
},
},
})
const user = await fetchUser('123')
expect(user.id).toBe('123')
expect(user.email).toBeTruthy()
})
})
REST-assured Inspired Testing (Node.js)
import request from 'supertest'
import app from '../src/app'
describe('Users API Integration Tests', () => {
let authToken: string
let createdUserId: string
beforeAll(async () => {
const resp = await request(app)
.post('/auth/login')
.send({ email: 'admin@test.com', password: 'admin123' })
authToken = resp.body.access_token
})
it('creates a user', async () => {
const resp = await request(app)
.post('/users')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Test User', email: 'newuser@test.com' })
.expect(201)
.expect('Content-Type', /json/)
expect(resp.body.id).toBeDefined()
expect(resp.body.email).toBe('newuser@test.com')
createdUserId = resp.body.id
})
it('returns 404 for missing user', async () => {
await request(app)
.get('/users/nonexistent-id')
.set('Authorization', `Bearer ${authToken}`)
.expect(404)
.expect(resp => expect(resp.body.error).toBe('User not found'))
})
})
API Testing Pyramid
| Level |
Tool |
Speed |
Coverage |
| Unit |
Jest/Vitest |
Fast |
Logic |
| Integration |
Supertest |
Medium |
Endpoints |
| Contract |
Pact |
Medium |
Contracts |
| E2E |
Playwright |
Slow |
Flows |
| Load |
k6 |
Variable |
Performance |