Modern applications increasingly rely on APIs as their backbone, making API testing critical for ensuring reliability, security, and performance. Effective API testing requires a multi-layered approach that validates functionality, security, performance, and contract compliance.
Unit tests verify individual API endpoints in isolation. For REST APIs, frameworks like Jest (Node.js) or pytest (Python) work well:
// Jest example for a user API endpoint
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
test('GET /users returns 200', async () => {
const response = await request(app).get('/users');
expect(response.statusCode).toBe(200);
});
test('POST /users creates new user', async () => {
const newUser = { name: 'Test User', email: 'test@example.com' };
const response = await request(app)
.post('/users')
.send(newUser);
expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('id');
});
});
Contract testing ensures APIs meet their specifications. Tools like Pact or OpenAPI validators help:
# OpenAPI specification example
paths:
/users:
get:
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
Validate against the spec using Schemathesis:
import schemathesis
schema = schemathesis.from_uri("http://api.example.com/openapi.json")
@schema.parametrize()
def test_api(case):
response = case.call()
assert response.status_code < 500
Integration tests verify how different services work together:
# Pytest example with service dependencies
def test_order_flow(api_client, inventory_service, payment_service):
# Setup test data
inventory_service.add_item(sku="TEST123", quantity=10)
# Test order creation
response = api_client.post("/orders", json={
"items": [{"sku": "TEST123", "quantity": 2}],
"payment_token": "test_token"
})
assert response.status_code == 201
assert inventory_service.get_item("TEST123")["quantity"] == 8
Instead of fixed test cases, generate random inputs to find edge cases:
from hypothesis import given
import hypothesis.strategies as st
@given(st.text(min_size=1))
def test_username_validation(username):
response = client.post("/users", json={"username": username})
assert response.status_code in [201, 400]
Inject failures to test resilience:
// Using Chaos Toolkit
{
"version": "1.0.0",
"title": "Simulate database failure during user creation",
"steady-state-hypothesis": {
"title": "Users can be created",
"probes": [
{
"type": "probe",
"name": "create-user",
"tolerance": 201,
"provider": {
"type": "http",
"url": "https://api.example.com/users",
"method": "POST"
}
}
]
},
"method": [
{
"type": "action",
"name": "block-db-access",
"provider": {
"type": "python",
"module": "chaosaws.rds.actions",
"func": "stop_rds_cluster",
"arguments": {
"cluster_identifier": "prod-db-cluster"
}
}
}
]
}
Locust or k6 are excellent for API load testing:
# Locust performance test
from locust import HttpUser, task, between
class ApiUser(HttpUser):
wait_time = between(1, 5)
@task
def get_users(self):
self.client.get("/users")
@task(3)
def create_user(self):
self.client.post("/users", json={
"name": "Test User",
"email": "test@example.com"
})
Test for common vulnerabilities:
# Using OWASP ZAP
docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-api-scan.py \
-t http://api.example.com/openapi.json \
-f openapi -r report.html
Verify JWT and OAuth flows:
// Test JWT expiration
const jwt = require('jsonwebtoken');
test('JWT expires after 1 hour', () => {
const token = jwt.sign({ user: 'test' }, 'secret', { expiresIn: '1h' });
const decoded = jwt.decode(token);
expect(decoded.exp - decoded.iat).toBe(3600);
});
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm test
- run: schemathesis run --checks all http://localhost:3000/openapi.json
Test against multiple environments:
#!/bin/bash
ENVIRONMENTS=("dev" "staging" "preprod")
echo "Testing $env environment"
API_URL="https://$env.api.example.com"
pytest tests/ --api-url $API_URL
done
// Java example
@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);
@Test
public void testExternalService() {
stubFor(get(urlEqualTo("/external/api"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"status\":\"success\"}")));
// Test code that calls the mock service
}
// Apollo Server mocking
const { ApolloServer, gql } = require('apollo-server');
const { addMocksToSchema } = require('@graphql-tools/mock');
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
`;
const server = new ApolloServer({
schema: addMocksToSchema({ schema: makeExecutableSchema({ typeDefs }) }),
});
# Validate traces in tests
def test_order_creates_trace(api_client, tracing_client):
response = api_client.post("/orders", json={...})
traces = tracing_client.get_traces()
assert any(trace.name == "POST /orders" for trace in traces)
// Test Prometheus metrics
const client = require('prom-client');
test('order metric increments', async () => {
const initial = client.register.getSingleMetric('orders_total').get().values[0].value;
await request(app).post('/orders').send({...});
const updated = client.register.getSingleMetric('orders_total').get().values[0].value;
expect(updated).toBe(initial + 1);
});
Modern API testing tools continue to evolve, with increasing support for AI-generated test cases and automatic anomaly detection. Stay current with tools like Keploy for API test generation and Tracetest for distributed tracing validation.