- name
- api-testing-rest
- description
- Comprehensive RESTful API testing patterns covering HTTP methods, status codes, request/response validation, authentication, error handling, and contract testing.
- license
- MIT
- metadata
- {"author":"thetestingacademy","version":"1.0.0","source":"https://qaskills.sh/skills/thetestingacademy/api-testing-rest"}
# API Testing REST Skill
You are an expert QA engineer specializing in REST API testing. When the user asks you to write, review, or design API tests, follow these detailed instructions.
## Core Principles
1. **Test the contract, not the implementation** -- Focus on request/response format, not server internals.
2. **Cover all HTTP methods** -- GET, POST, PUT, PATCH, DELETE each have different semantics.
3. **Validate status codes** -- Correct status codes are part of the API contract.
4. **Test error paths** -- Bad requests and edge cases are as important as happy paths.
5. **Assert on response structure** -- JSON schema validation ensures consistency.
## REST API Fundamentals
### HTTP Methods and Their Semantics
```
GET - Retrieve resource(s), safe and idempotent
POST - Create new resource, not idempotent
PUT - Replace entire resource, idempotent
PATCH - Partial update, idempotent
DELETE - Remove resource, idempotent
HEAD - Same as GET but no response body
OPTIONS - Get supported methods for resource
```
### HTTP Status Codes
```
Success (2xx):
200 OK - Successful GET, PUT, PATCH, DELETE
201 Created - Successful POST, resource created
204 No Content - Successful DELETE (no body returned)
Client Error (4xx):
400 Bad Request - Invalid request body or parameters
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Resource conflict (duplicate email)
422 Unprocessable - Validation error
Server Error (5xx):
500 Internal Error - Server error
503 Service Unavailable - Service down or overloaded
```
## Testing Patterns with Different Tools
### 1. JavaScript/TypeScript with Axios/Fetch
```typescript
// api-client.ts
import axios from 'axios';
export class ApiClient {
private baseURL = 'https://api.example.com';
private authToken: string | null = null;
setAuthToken(token: string) {
this.authToken = token;
}
private getHeaders() {
return {
'Content-Type': 'application/json',
...(this.authToken && { Authorization: `Bearer ${this.authToken}` }),
};
}
async get(endpoint: string, params = {}) {
const response = await axios.get(`${this.baseURL}${endpoint}`, {
headers: this.getHeaders(),
params,
});
return response;
}
async post(endpoint: string, data: any) {
const response = await axios.post(`${this.baseURL}${endpoint}`, data, {
headers: this.getHeaders(),
});
return response;
}
async put(endpoint: string, data: any) {
const response = await axios.put(`${this.baseURL}${endpoint}`, data, {
headers: this.getHeaders(),
});
return response;
}
async delete(endpoint: string) {
const response = await axios.delete(`${this.baseURL}${endpoint}`, {
headers: this.getHeaders(),
});
return response;
}
}
```
```typescript
// users.api.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { ApiClient } from './api-client';
describe('Users API', () => {
const api = new ApiClient();
let createdUserId: string;
beforeAll(async () => {
// Authenticate before running tests
const authResponse = await api.post('/auth/login', {
email: 'test@example.com',
password: 'password123',
});
api.setAuthToken(authResponse.data.token);
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'user',
};
const response = await api.post('/api/users', userData);
// Assert status code
expect(response.status).toBe(201);
// Assert response structure
expect(response.data).toHaveProperty('id');
expect(response.data).toHaveProperty('email', userData.email);
expect(response.data).toHaveProperty('name', userData.name);
expect(response.data).toHaveProperty('createdAt');
// Assert response types
expect(typeof response.data.id).toBe('string');
expect(response.data.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
// Save for cleanup
createdUserId = response.data.id;
});
it('should return 400 for invalid email', async () => {
try {
await api.post('/api/users', {
email: 'invalid-email',
name: 'Test',
});
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(400);
expect(error.response.data).toHaveProperty('error');
expect(error.response.data.error).toContain('email');
}
});
it('should return 409 for duplicate email', async () => {
const userData = {
email: 'duplicate@example.com',
name: 'Duplicate User',
};
// Create first user
await api.post('/api/users', userData);
// Attempt to create duplicate
try {
await api.post('/api/users', userData);
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(409);
expect(error.response.data.error).toContain('already exists');
}
});
});
describe('GET /api/users/:id', () => {
it('should retrieve user by ID', async () => {
const response = await api.get(`/api/users/${createdUserId}`);
expect(response.status).toBe(200);
expect(response.data.id).toBe(createdUserId);
expect(response.data).toHaveProperty('email');
expect(response.data).toHaveProperty('name');
});
it('should return 404 for non-existent user', async () => {
try {
await api.get('/api/users/non-existent-id');
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
});
describe('GET /api/users', () => {
it('should list all users', async () => {
const response = await api.get('/api/users');
expect(response.status).toBe(200);
expect(Array.isArray(response.data)).toBe(true);
expect(response.data.length).toBeGreaterThan(0);
// Validate structure of first user
const firstUser = response.data[0];
expect(firstUser).toHaveProperty('id');
expect(firstUser).toHaveProperty('email');
expect(firstUser).toHaveProperty('name');
});
it('should support pagination', async () => {
const response = await api.get('/api/users', {
page: 1,
limit: 10,
});
expect(response.status).toBe(200);
expect(response.data).toHaveProperty('items');
expect(response.data).toHaveProperty('total');
expect(response.data).toHaveProperty('page', 1);
expect(response.data).toHaveProperty('limit', 10);
expect(response.data.items.length).toBeLessThanOrEqual(10);
});
it('should support filtering', async () => {
const response = await api.get('/api/users', {
role: 'admin',
});
expect(response.status).toBe(200);
expect(Array.isArray(response.data)).toBe(true);
// All users should be admins
response.data.forEach((user: any) => {
expect(user.role).toBe('admin');
});
});
});
describe('PUT /api/users/:id', () => {
it('should update user completely', async () => {
const updatedData = {
email: 'updated@example.com',
name: 'Updated Name',
role: 'admin',
};
const response = await api.put(`/api/users/${createdUserId}`, updatedData);
expect(response.status).toBe(200);
expect(response.data.email).toBe(updatedData.email);
expect(response.data.name).toBe(updatedData.name);
expect(response.data.role).toBe(updatedData.role);
});
it('should return 404 for non-existent user', async () => {
try {
await api.put('/api/users/non-existent', { name: 'Test' });
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
});
describe('DELETE /api/users/:id', () => {
it('should delete user', async () => {
const response = await api.delete(`/api/users/${createdUserId}`);
expect(response.status).toBe(204);
// Verify deletion
try {
await api.get(`/api/users/${createdUserId}`);
fail('User should be deleted');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
it('should return 404 when deleting non-existent user', async () => {
try {
await api.delete('/api/users/non-existent');
fail('Should have thrown an error');
} catch (error: any) {
expect(error.response.status).toBe(404);
}
});
});
});
```
### 2. Python with requests/pytest
```python
# api_client.py
import requests
from typing import Dict, Any, Optional
class ApiClient:
def __init__(self, base_url: str):
self.base_url = base_url
self.session = requests.Session()
self.auth_token: Optional[str] = None
def set_auth_token(self, token: str):
"""Set authentication token for all requests."""
self.auth_token = token
self.session.headers.update({'Authorization': f'Bearer {token}'})
def get(self, endpoint: str, params: Optional[Dict] = None) -> requests.Response:
"""Perform GET request."""
url = f"{self.base_url}{endpoint}"
return self.session.get(url, params=params)
def post(self, endpoint: str, data: Dict[str, Any]) -> requests.Response:
"""Perform POST request."""
url = f"{self.base_url}{endpoint}"
return self.session.post(url, json=data)
def put(self, endpoint: str, data: Dict[str, Any]) -> requests.Response:
"""Perform PUT request."""
url = f"{self.base_url}{endpoint}"
return self.session.put(url, json=data)
def delete(self, endpoint: str) -> requests.Response:
"""Perform DELETE request."""
url = f"{self.base_url}{endpoint}"
return self.session.delete(url)
```
```python
# test_users_api.py
import pytest
from api_client import ApiClient
@pytest.fixture(scope="module")
def api_client():
"""Create API client and authenticate."""
client = ApiClient("https://api.example.com")
# Authenticate
response = client.post("/auth/login", {
"email": "test@example.com",
"password": "password123"
})
assert response.status_code == 200
client.set_auth_token(response.json()["token"])
return client
@pytest.fixture
def created_user(api_client):
"""Create a test user and clean up after test."""
response = api_client.post("/api/users", {
GitHubで見る