API Testing
Master API testing strategies and automation to ensure your APIs are reliable, secure, and performant.
When to Use This Skill
-
Setting up API testing infrastructure
-
Integrating Postman MCP for automation
-
Writing unit and integration tests for endpoints
-
Implementing contract testing and validation
-
Setting up CI/CD pipeline testing
-
Testing GraphQL queries and mutations
-
Automating API test execution
-
Building test suites with multiple frameworks
Testing Pyramid for APIs
Structure your tests from bottom to top:
E2E Tests (Postman collections, full flow)
/ \
/ \
Integration Tests API Tests
(DB, caching, etc.) (endpoint behavior)
/
/
Unit Tests (business logic, validation)
- Unit Tests (Foundation)
Test individual functions and business logic in isolation:
pytest example
import pytest from app.models import User
def test_user_password_hashing(): user = User(email="test@example.com") user.set_password("mypassword") assert user.password != "mypassword" assert user.check_password("mypassword") is True assert user.check_password("wrongpassword") is False
def test_user_email_validation(): with pytest.raises(ValueError): User(email="invalid-email")
- Integration Tests
Test API endpoints with real dependencies (database, caching):
pytest example with fixtures
import pytest from fastapi.testclient import TestClient from app import app from app.db import get_db
@pytest.fixture def client(db_session): def override_get_db(): return db_session app.dependency_overrides[get_db] = override_get_db return TestClient(app)
def test_create_user(client): response = client.post("/api/users", json={ "email": "newuser@example.com", "name": "New User" }) assert response.status_code == 201 assert response.json()["email"] == "newuser@example.com"
def test_get_user(client, db_session): # Create test user from app.models import User user = User(email="test@example.com", name="Test") db_session.add(user) db_session.commit()
response = client.get(f"/api/users/{user.id}")
assert response.status_code == 200
assert response.json()["name"] == "Test"
3. End-to-End Tests
Test full user workflows using Postman collections:
-
Import from Postman collections
-
Run with Newman CLI
-
Execute in CI/CD pipelines
-
Test realistic user scenarios
REST API Testing with Pytest
Basic Endpoint Testing
import pytest from fastapi.testclient import TestClient from app import app
client = TestClient(app)
class TestUserAPI: def test_list_users(self): response = client.get("/api/users") assert response.status_code == 200 assert isinstance(response.json(), list)
def test_create_user_success(self):
response = client.post("/api/users", json={
"email": "newuser@example.com",
"name": "New User",
"password": "secure123"
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "newuser@example.com"
assert "password" not in data # Never expose passwords
def test_create_user_validation_error(self):
response = client.post("/api/users", json={
"email": "invalid-email",
"name": "Test"
})
assert response.status_code == 422
assert "email" in response.json()["detail"][0]["loc"]
def test_get_nonexistent_user(self):
response = client.get("/api/users/99999")
assert response.status_code == 404
def test_update_user(self):
# Create user first
create_resp = client.post("/api/users", json={
"email": "user@example.com",
"name": "Original"
})
user_id = create_resp.json()["id"]
# Update user
response = client.patch(f"/api/users/{user_id}", json={
"name": "Updated"
})
assert response.status_code == 200
assert response.json()["name"] == "Updated"
def test_delete_user(self):
# Create user
create_resp = client.post("/api/users", json={
"email": "deleteme@example.com",
"name": "Delete Me"
})
user_id = create_resp.json()["id"]
# Delete
response = client.delete(f"/api/users/{user_id}")
assert response.status_code == 204
# Verify deletion
get_resp = client.get(f"/api/users/{user_id}")
assert get_resp.status_code == 404
Parametrized Testing
import pytest
@pytest.mark.parametrize("status_code,expected_status", [ (200, 200), (201, 201), (400, 400), (404, 404), ]) def test_status_codes(status_code, expected_status): response = client.get(f"/api/test?status={status_code}") assert response.status_code == expected_status
GraphQL Testing with Jest
Basic Query Testing
import { gql } from '@apollo/client'; import { ApolloClient } from '@apollo/client';
const client = new ApolloClient({ uri: 'http://localhost:4000/graphql' });
describe('GraphQL User Queries', () => {
it('should fetch user by ID', async () => {
const query = gql query GetUser($id: ID!) { user(id: $id) { id name email } } ;
const result = await client.query({
query,
variables: { id: '1' }
});
expect(result.data.user).toBeDefined();
expect(result.data.user.email).toBe('user@example.com');
});
it('should list users with pagination', async () => {
const query = gql query ListUsers($first: Int!) { users(first: $first) { edges { node { id name } } pageInfo { hasNextPage } } } ;
const result = await client.query({
query,
variables: { first: 10 }
});
expect(result.data.users.edges).toHaveLength(10);
expect(result.data.users.pageInfo.hasNextPage).toBeDefined();
}); });
describe('GraphQL Mutations', () => {
it('should create user with validation', async () => {
const mutation = gql mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { user { id email } errors { field message } success } } ;
const result = await client.mutate({
mutation,
variables: {
input: { email: 'newuser@example.com', name: 'New' }
}
});
expect(result.data.createUser.success).toBe(true);
expect(result.data.createUser.user.id).toBeDefined();
expect(result.data.createUser.errors).toHaveLength(0);
});
it('should return errors for invalid input', async () => {
const mutation = gql mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { user { id } errors { field message } success } } ;
const result = await client.mutate({
mutation,
variables: {
input: { email: 'invalid-email', name: '' }
}
});
expect(result.data.createUser.success).toBe(false);
expect(result.data.createUser.errors.length).toBeGreaterThan(0);
}); });
Postman MCP Integration
Installation
The Postman MCP server enables running Postman collections through Claude Code:
Method 1: Via Smithery (Recommended)
npx -y @smithery/cli install mcp-postman --client claude
Method 2: Manual Installation
git clone https://github.com/shannonlal/mcp-postman.git cd mcp-postman pnpm install pnpm build
Configuration
Add to ~/.config/claude/config.json :
{ "mcpServers": { "postman": { "command": "node", "args": ["/path/to/mcp-postman/build/index.js"], "env": { "POSTMAN_API_KEY": "your-api-key-here" } } } }
Using Postman MCP
Once configured, interact with Postman from Claude Code:
User: "Run the user-api collection from my Postman workspace"
Claude can now:
- List available collections
- Execute collections with specific environments
- Parse test results
- Run in CI/CD pipelines
- Generate test reports
Newman Automation
Newman CLI runs Postman collections headlessly for CI/CD:
Installation
npm install -g newman npm install -g newman-reporter-html
Running Collections
Basic execution
newman run collection.json
With environment
newman run collection.json -e environment.json
With variables
newman run collection.json --global-var "base_url=https://api.example.com"
Multiple reporters
newman run collection.json
--reporters cli,json,html
--reporter-json-export results.json
--reporter-html-export report.html
CI/CD Integration Example
GitHub Actions:
name: API Tests on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install -g newman
- run: |
newman run postman/collection.json
-e postman/environment.json
--reporters cli,json
--reporter-json-export results.json
Contract Testing
Validate APIs match their specifications:
OpenAPI Validation
Using openapi-spec-validator
from openapi_spec_validator import validate_spec import json
with open('openapi.json') as f: spec = json.load(f)
try: validate_spec(spec) print("OpenAPI spec is valid") except Exception as e: print(f"Spec validation error: {e}")
Pact Consumer/Provider Testing
// Consumer test with Pact import { Pact } from '@pact-foundation/pact';
describe('User Consumer', () => { const provider = new Pact({ consumer: 'UserApp', provider: 'UserAPI' });
it('should fetch user', () => { return provider .addInteraction({ state: 'user exists', uponReceiving: 'a request for user 123', withRequest: { method: 'GET', path: '/api/users/123' }, willRespondWith: { status: 200, body: { id: 123, name: 'John', email: 'john@example.com' } } }) .then(() => fetch('http://localhost:8080/api/users/123')) .then(response => response.json()) .then(data => expect(data.email).toBe('john@example.com')); }); });
Authentication Testing
def test_bearer_token_required(client): response = client.get("/api/users") assert response.status_code == 401
def test_valid_bearer_token(client): headers = {"Authorization": "Bearer valid-token-123"} response = client.get("/api/users", headers=headers) assert response.status_code == 200
def test_invalid_bearer_token(client): headers = {"Authorization": "Bearer invalid-token"} response = client.get("/api/users", headers=headers) assert response.status_code == 401
def test_api_key_validation(client): headers = {"X-API-Key": "valid-key"} response = client.get("/api/users", headers=headers) assert response.status_code == 200
headers = {"X-API-Key": "invalid-key"}
response = client.get("/api/users", headers=headers)
assert response.status_code == 401
Performance Testing
Load testing with locust
from locust import HttpUser, task, constant
class APIUser(HttpUser): wait_time = constant(1)
@task
def list_users(self):
self.client.get("/api/users")
@task
def get_user(self):
self.client.get("/api/users/1")
Run with: locust -f locustfile.py -u 100 -r 10
Best Practices
-
Test at Multiple Levels: Unit → Integration → E2E
-
Use Test Fixtures: Reusable setup/teardown for consistent tests
-
Parametrize Tests: Test multiple scenarios efficiently
-
Mock External APIs: Isolate your tests from dependencies
-
Automate in CI/CD: Run tests on every commit
-
Document Test Cases: Clear test names and docstrings
-
Test Error Cases: Not just happy paths
-
Use Postman MCP: Automate Postman collection execution
-
Monitor Test Coverage: Aim for >80% coverage
-
Test Performance: Regular load and performance tests
Common Pitfalls to Avoid
-
Testing implementation details instead of behavior
-
Flaky tests that fail intermittently
-
Not testing error responses
-
Ignoring authentication/authorization tests
-
Missing edge cases and boundary conditions
-
Slow test suites (optimize or split)
-
Testing external APIs without mocking
-
No test data management strategy
Cross-Skill References
-
rest-api-design skill - For REST endpoint patterns to test
-
graphql-api-design skill - For GraphQL query patterns to test
-
api-architecture skill - For security and monitoring strategies
Reference files for this skill are planned for a future release.