Design a comprehensive API testing strategy covering contract tests, integration tests, load tests, and security scans. Covers tooling, test pyramids, and CI/CD integration for production-grade APIs.
API Testing Strategy is a guide to designing, executing, and maintaining a reliable, scalable, and meaningful set of tests for APIs across development, staging, and production environments. It applies when multiple services communicate via HTTP APIs, especially in microservices architectures where API contracts evolve independently. Use this strategy when a team needs to reduce integration bugs, validate new features quickly, and ensure backward compatibility across releases.
Define which APIs to test based on their role and risk profile. Focus on core business APIs, external integrations, and gateway endpoints that serve multiple clients. Test request-response cycles for typical user flows, not just individual endpoints.
Use API versioning as a key signal: every change to a versioned API (e.g., /v1/users, /v2/users) must have a test suite that verifies backward compatibility, error handling, and new fields. For instance, a request to /v2/users must handle all fields from /v1/users and validate new fields with default values.
Use a test pyramid approach:
Use curl and jq for ad-hoc testing and scriptable test pipelines. All tests should be idempotent and self-contained, with no reliance on external state between runs.
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Smith",
"email": "[email protected]",
"preferences": {
"theme": "dark",
"notifications": true
}
}' \
http://localhost:8080/v1/users
Validate responses with jq:
curl -s http://localhost:8080/v1/users/123 | jq '.name, .email, .preferences.theme'
Use test databases with psql for consistent state. Initialize a test database with a known schema and seed data:
psql -d test_db -c "
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO users (name, email) VALUES
('Alice', '[email protected]'),
('Bob', '[email protected]');
"
Run tests using httpx, a command-line tool that supports request templates, assertions, and test suites.
Example test file test-users.yaml:
- name: Create user with valid data
request:
method: POST
url: http://localhost:8080/v1/users
headers:
Content-Type: application/json
body:
name: "Charlie Brown"
email: "[email protected]"
assert:
- status_code: 201
- json:
name: "Charlie Brown"
email: "[email protected]"
id: 4
created_at: "2024-03-15T10:30:00Z"
- header:
Location: "http://localhost:8080/v1/users/4"
setup:
- psql -d test_db -c "DELETE FROM users WHERE email = '[email protected]'"
teardown:
- psql -d test_db -c "DELETE FROM users WHERE email = '[email protected]'"
Run all tests:
httpx -f test-users.yaml -e staging
The -e staging flag loads environment variables from staging.env, which may include BASE_URL, API_KEY, and DATABASE_URL.
Use jsonschema to validate response payloads against a shared schema definition.
Example schema: schemas/user.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"preferences": {
"type": "object",
"properties": {
"theme": { "type": "string", "enum": ["light", "dark", "auto"] },
"notifications": { "type": "boolean" }
},
"required": ["theme"]
},
"created_at": { "type": "string", "format": "date-time" }
},
"required": ["id", "name", "email", "created_at"]
}
Validate a response:
curl -s http://localhost:8080/v1/users/1 | \
jsonschema -s schemas/user.json
Failures include:
{
"message": "Validation failed",
"errors": [
{
"message": "Missing required property: preferences.notifications",
"path": "preferences",
"schema": "user.json"
},
{
"message": "Invalid value: 'purple' for theme",
"path": "preferences.theme",
"schema": "user.json"
}
]
}
Define environment-specific configurations in .env files:
# .env.staging
BASE_URL=https://staging.api.example.com
API_KEY=staging-abc123
DATABASE_URL=postgresql://staging:5432/app_db
LOG_LEVEL=info
Use env to inject values into test runs:
env $(cat .env.staging | xargs) httpx -f test-users.yaml
Each test run should start with a clean state. Use docker-compose to spin up services:
# docker-compose.test.yml
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: test_db
POSTGRES_USER: test
POSTGRES_PASSWORD: test
volumes:
- ./test-data.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5433:5432"
Run:
docker-compose -f docker-compose.test.yml up -d
# Run tests
httpx -f test-users.yaml
# Tear down
docker-compose -f docker-compose.test.yml down
For APIs that modify state across requests, use test state snapshots. After a sequence of operations, record the final state:
# After a test sequence, capture current state
psql -d test_db -c "SELECT * FROM users;" > test-state.sql
Compare against a baseline using diff:
diff test-state.sql expected-state.sql
Failures indicate unexpected changes in database state, such as missing rows, incorrect timestamps, or wrong values.
Use jwt (from the pyjwt package) to decode and validate bearer tokens.
Example: verify a JWT returned from /auth/login:
curl -s http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "secret"}' \
| jq -r '.token' \
| jwt decode
Expected output:
{
"sub": "123",
"email": "[email protected]",
"exp": 1700000000,
"iat": 1699999000,
"roles": ["user", "admin"]
}
Verify token signature and expiration:
jwt verify \
--public-key ./jwt-public-key.pem \
--expected-iss "api.example.com" \
--min-iat 30 \
--max-exp 3600 \
token.jwt
For APIs with Authorization: Bearer <token>, verify that users with different roles access the correct resources.
Test a request to /admin/users:
curl -s \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
http://localhost:8080/admin/users \
| jq '.users[] | select(.role == "admin") | .name'
Failures include:
Add a test job in a CI pipeline using yaml (e.g., GitHub Actions):
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up PostgreSQL
run: |
docker run -d --name test-db -p 5432:5432 -e POSTGRES_DB=test_db postgres:15
- name: Start API service
run: |
docker build -t api-service .
docker run -d -p 8080:8080 --link test-db api-service
- name: Wait for API
run: |
until curl -s http://localhost:8080/health; do sleep 1; done
- name: Run API tests
run: |
httpx -f tests/api.yaml -e .env.test
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
test-results.json
test-reports/
Use a test failure log with timestamp, request, response, and error details:
{
"test": "Create user with valid data",
"timestamp": "2024-03-15T10:35:00Z",
"request": {
"method": "POST",
"url": "http://localhost:8080/v1/users",
"headers": { "Content-Type": "application/json" },
"body": { "name": "Dora", "email": "[email protected]" }
},
"response": {
"status": 500,
"headers": { "Content-Type": "application/json" },
"body": {
"error": "Internal server error",
"code": "INTERNAL_ERROR",
"details": [ "Database connection failed", "Query timeout" ]
}
},
"assertions": [
{ "check": "status_code", "expected": 201, "actual": 500, "passed": false },
{ "check": "json.id", "expected": "integer", "actual": "null", "passed": false }
]
}
Store this log in a database or as a file in a test-results/ directory, and use grep to find regressions:
grep -r "Internal server error" test-results/
required field in a response is often missed because the test only checks the top-level keys.jsonschema fails silently when a null value is expected to be object, but the API returns null instead.jq to verify nested structures before passing to jsonschema.status_code and response_body Assertionsstatus_code: 200 but the body contains an error object ({"error": "Not found"}).status_code with json or body validation.test-data.sql, but a test assumes a users table with id as SERIAL, which is not present.psql -c "SELECT * FROM pg_tables WHERE tablename = 'users'" to validate schema before running tests.401 or 403 on expired tokens.exp and iat claims and ensure client logic refreshes tokens before expiration.created_at to be within 10 seconds of now().TZ=UTC and date command to standardize timestamps across tests.teardown scripts to truncate tables and reset sequences.Run the full test suite before every release. Use httpx with --fail-fast and --parallel to reduce execution time:
httpx -f tests/ -e staging --fail-fast --parallel 4
Add a healthcheck test to verify API availability:
- name: Health check
request:
method: GET
url: http://localhost:8080/health
assert:
- status_code: 200
- json:
status: "ok"
version: "v1.2.3"
services:
- name: database
status: "up"
latency_ms: 12
- name: cache
status: "up"
latency_ms: 3
Run this before deploying new code, and fail the build if any test fails. Keep the test suite as a living document, updated with every API change.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy