API Testing Complete Guide: Postman, REST Client, and Automated Testing
Introduction
Robust API testing ensures reliability, performance, and correctness of backend services. This guide covers manual API testing with Postman and VS Code REST Client, automated testing with Newman and Jest, contract testing with Pact, and integrating API tests into CI/CD pipelines for continuous validation.
Postman Fundamentals
Creating Collections
Basic Request:
{
"name": "Get Users",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://api.contoso.com/v1/users",
"protocol": "https",
"host": ["api", "contoso", "com"],
"path": ["v1", "users"]
}
}
}
Request with Authentication:
// Collection Variables
baseUrl = https://api.contoso.com
apiKey = your-api-key-here
// Request Header
{
"Authorization": "Bearer {{accessToken}}",
"X-API-Key": "{{apiKey}}"
}
// Pre-request Script (OAuth2 token)
pm.sendRequest({
url: 'https://auth.contoso.com/oauth/token',
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
mode: 'urlencoded',
urlencoded: [
{key: 'grant_type', value: 'client_credentials'},
{key: 'client_id', value: pm.environment.get('clientId')},
{key: 'client_secret', value: pm.environment.get('clientSecret')}
]
}
}, function (err, response) {
if (response.code === 200) {
pm.environment.set('accessToken', response.json().access_token);
}
});
Test Scripts
Response Validation:
// Status code validation
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// Response time check
pm.test("Response time is less than 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// JSON schema validation
const schema = {
"type": "object",
"properties": {
"userId": { "type": "number" },
"username": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["userId", "username"]
};
pm.test("Schema is valid", function () {
pm.response.to.have.jsonSchema(schema);
});
// Body content validation
pm.test("User has correct email domain", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.email).to.include('@contoso.com');
});
Data-Driven Testing:
# users.csv
userId,expectedName,expectedEmail
1,John Doe,john@contoso.com
2,Jane Smith,jane@contoso.com
// Collection Runner → Select CSV file
pm.test("User data matches CSV", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.name).to.eql(pm.iterationData.get("expectedName"));
pm.expect(jsonData.email).to.eql(pm.iterationData.get("expectedEmail"));
});
Environment Management
Development Environment:
{
"name": "Development",
"values": [
{
"key": "baseUrl",
"value": "https://api-dev.contoso.com",
"enabled": true
},
{
"key": "clientId",
"value": "dev-client-id",
"enabled": true
}
]
}
Switch Environments via CLI:
newman run collection.json \
-e dev-environment.json \
--reporters cli,json \
--reporter-json-export results.json
VS Code REST Client
Basic Requests
.http File:
### Get all users
GET https://api.contoso.com/v1/users HTTP/1.1
Authorization: Bearer {{$dotenv TOKEN}}
### Create user
POST https://api.contoso.com/v1/users HTTP/1.1
Content-Type: application/json
Authorization: Bearer {{$dotenv TOKEN}}
{
"username": "johndoe",
"email": "john@contoso.com",
"role": "user"
}
### Update user
PUT https://api.contoso.com/v1/users/123 HTTP/1.1
Content-Type: application/json
Authorization: Bearer {{$dotenv TOKEN}}
{
"email": "john.doe@contoso.com"
}
### Delete user
DELETE https://api.contoso.com/v1/users/123 HTTP/1.1
Authorization: Bearer {{$dotenv TOKEN}}
Variables and Environment
Environment Variables (.env):
TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
BASE_URL=https://api.contoso.com
API_VERSION=v1
Request Variables:
@baseUrl = {{$dotenv BASE_URL}}
@apiVersion = {{$dotenv API_VERSION}}
@contentType = application/json
### Get user by ID
@userId = 123
GET {{baseUrl}}/{{apiVersion}}/users/{{userId}} HTTP/1.1
Content-Type: {{contentType}}
Authorization: Bearer {{$dotenv TOKEN}}
### Use response in next request
@authToken = {{login.response.body.$.token}}
POST {{baseUrl}}/{{apiVersion}}/protected HTTP/1.1
Authorization: Bearer {{authToken}}
Pre-Request Scripts
Dynamic Headers:
# @name login
POST https://api.contoso.com/v1/auth/login HTTP/1.1
Content-Type: application/json
{
"username": "admin",
"password": "password123"
}
###
# Extract token from login response
@token = {{login.response.body.$.accessToken}}
GET https://api.contoso.com/v1/users HTTP/1.1
Authorization: Bearer {{token}}
Automated Testing with Newman
CLI Execution
Basic Run:
# Install Newman
npm install -g newman
# Run collection
newman run collection.json \
--environment production.json \
--reporters cli,htmlextra \
--reporter-htmlextra-export report.html
Advanced Options:
newman run collection.json \
--environment prod.json \
--globals globals.json \
--iteration-count 10 \
--delay-request 1000 \
--timeout-request 30000 \
--bail \
--reporters cli,json,junit \
--reporter-junit-export results.xml
CI/CD Integration
GitHub Actions:
name: API Tests
on:
push:
branches: [main]
schedule:
- cron: '0 */6 * * *' # Every 6 hours
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 Newman
run: npm install -g newman newman-reporter-htmlextra
- name: Run API Tests
run: |
newman run postman/collection.json \
--environment postman/prod.json \
--reporters cli,htmlextra,junit \
--reporter-htmlextra-export test-results/report.html \
--reporter-junit-export test-results/results.xml
env:
API_KEY: ${{ secrets.API_KEY }}
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always()
with:
name: API Test Results
path: test-results/results.xml
reporter: jest-junit
- name: Upload HTML Report
uses: actions/upload-artifact@v3
if: always()
with:
name: test-report
path: test-results/report.html
Azure DevOps Pipeline:
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
- script: npm install -g newman newman-reporter-htmlextra
displayName: 'Install Newman'
- script: |
newman run $(System.DefaultWorkingDirectory)/postman/collection.json \
--environment $(System.DefaultWorkingDirectory)/postman/prod.json \
--reporters cli,htmlextra,junit \
--reporter-htmlextra-export $(Build.ArtifactStagingDirectory)/report.html \
--reporter-junit-export $(Build.ArtifactStagingDirectory)/results.xml
displayName: 'Run API Tests'
env:
API_KEY: $(API_KEY)
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(Build.ArtifactStagingDirectory)/results.xml'
condition: always()
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'api-test-results'
condition: always()
Unit Testing APIs with Jest
Setup
npm install --save-dev jest supertest @types/jest
jest.config.js:
module.exports = {
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/'],
testMatch: ['**/__tests__/**/*.test.js'],
collectCoverageFrom: ['src/**/*.js'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Testing Express API
__tests__/users.test.js:
const request = require('supertest');
const app = require('../src/app');
const { setupDatabase, clearDatabase } = require('./fixtures/db');
beforeAll(async () => {
await setupDatabase();
});
afterAll(async () => {
await clearDatabase();
});
describe('Users API', () => {
describe('GET /api/users', () => {
test('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.set('Authorization', 'Bearer valid-token')
.expect(200);
expect(response.body).toHaveLength(3);
expect(response.body[0]).toHaveProperty('id');
expect(response.body[0]).toHaveProperty('username');
});
test('should return 401 without authentication', async () => {
await request(app)
.get('/api/users')
.expect(401);
});
});
describe('POST /api/users', () => {
test('should create new user', async () => {
const newUser = {
username: 'testuser',
email: 'test@contoso.com',
password: 'SecurePass123!'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.username).toBe(newUser.username);
expect(response.body).not.toHaveProperty('password');
});
test('should validate required fields', async () => {
const response = await request(app)
.post('/api/users')
.send({ username: 'testuser' })
.expect(400);
expect(response.body.errors).toContainEqual(
expect.objectContaining({ field: 'email' })
);
});
});
describe('PUT /api/users/:id', () => {
test('should update existing user', async () => {
const response = await request(app)
.put('/api/users/1')
.set('Authorization', 'Bearer valid-token')
.send({ email: 'updated@contoso.com' })
.expect(200);
expect(response.body.email).toBe('updated@contoso.com');
});
});
describe('DELETE /api/users/:id', () => {
test('should delete user', async () => {
await request(app)
.delete('/api/users/1')
.set('Authorization', 'Bearer admin-token')
.expect(204);
// Verify deletion
await request(app)
.get('/api/users/1')
.expect(404);
});
});
});
Mocking External APIs
__tests__/orders.test.js:
const nock = require('nock');
const request = require('supertest');
const app = require('../src/app');
describe('Orders API with External Dependencies', () => {
afterEach(() => {
nock.cleanAll();
});
test('should process order with payment gateway', async () => {
// Mock payment gateway API
nock('https://payment-gateway.contoso.com')
.post('/api/v1/charge')
.reply(200, { transactionId: 'txn_123', status: 'success' });
// Mock inventory service
nock('https://inventory-service.contoso.com')
.post('/api/v1/reserve')
.reply(200, { reserved: true });
const order = {
customerId: 1,
items: [{ productId: 101, quantity: 2 }],
paymentMethod: 'credit_card'
};
const response = await request(app)
.post('/api/orders')
.send(order)
.expect(201);
expect(response.body.status).toBe('confirmed');
expect(response.body.transactionId).toBe('txn_123');
});
});
Contract Testing with Pact
Provider Contract
__tests__/pact/provider.test.js:
const { Verifier } = require('@pact-foundation/pact');
const app = require('../src/app');
describe('Pact Verification', () => {
test('validates provider against consumer contracts', async () => {
const server = app.listen(3000);
try {
await new Verifier({
provider: 'UserService',
providerBaseUrl: 'http://localhost:3000',
pactUrls: [
'https://pact-broker.contoso.com/pacts/provider/UserService/consumer/WebApp/latest'
],
publishVerificationResult: true,
providerVersion: process.env.GIT_COMMIT,
stateHandlers: {
'user with ID 1 exists': async () => {
// Setup test data
await createTestUser({ id: 1, username: 'johndoe' });
}
}
}).verifyProvider();
} finally {
server.close();
}
});
});
Consumer Contract
__tests__/pact/consumer.test.js:
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { getUserById } = require('../src/userClient');
const provider = new PactV3({
consumer: 'WebApp',
provider: 'UserService'
});
describe('User Client', () => {
test('retrieves user by ID', async () => {
await provider
.given('user with ID 1 exists')
.uponReceiving('a request for user 1')
.withRequest({
method: 'GET',
path: '/api/users/1',
headers: { 'Accept': 'application/json' }
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: MatchersV3.integer(1),
username: MatchersV3.string('johndoe'),
email: MatchersV3.regex('.*@.*', 'john@contoso.com')
}
})
.executeTest(async (mockServer) => {
const user = await getUserById(1, mockServer.url);
expect(user.id).toBe(1);
expect(user.username).toBe('johndoe');
});
});
});
Performance Testing
Apache Bench (ab)
# Basic load test
ab -n 1000 -c 10 https://api.contoso.com/v1/users
# With authentication
ab -n 1000 -c 10 \
-H "Authorization: Bearer token123" \
https://api.contoso.com/v1/users
# POST request
ab -n 1000 -c 10 \
-T application/json \
-p payload.json \
https://api.contoso.com/v1/users
k6 Load Testing
load-test.js:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '30s', target: 0 } // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% requests < 500ms
http_req_failed: ['rate<0.01'] // Error rate < 1%
}
};
export default function () {
const response = http.get('https://api.contoso.com/v1/users', {
headers: { 'Authorization': 'Bearer token123' }
});
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500
});
sleep(1);
}
Run Load Test:
k6 run load-test.js
# Cloud execution
k6 cloud load-test.js
Best Practices
- Test Pyramid: More unit tests, fewer integration tests, minimal E2E tests
- Idempotency: Ensure tests can run multiple times without side effects
- Test Data Isolation: Each test creates/cleans up its own data
- Mock External Services: Use nock or similar to avoid dependencies
- Environment Variables: Never hardcode credentials in collections
- Version Control: Commit Postman collections and
.httpfiles - Continuous Validation: Run API tests on every deployment
- Contract Testing: Validate API compatibility between services
Troubleshooting
Postman SSL Errors:
# Disable SSL verification (development only)
Settings → General → SSL certificate verification = OFF
Newman Timeout Issues:
# Increase timeout
newman run collection.json --timeout-request 60000
Jest Watch Mode:
# Run tests in watch mode
npm test -- --watch
Key Takeaways
- Postman provides comprehensive manual testing with scripting capabilities
- VS Code REST Client offers lightweight, version-controlled API testing
- Newman enables automated API testing in CI/CD pipelines
- Jest with supertest validates API behavior at the unit level
- Contract testing with Pact ensures API compatibility across services
Next Steps
- Explore GraphQL testing with Postman and GraphQL Playground
- Implement API monitoring with Postman Monitors or Datadog Synthetics
- Learn gRPC testing with grpcurl and Postman
- Investigate chaos engineering for API resilience testing
Additional Resources
Test early, test often, automate everything.