This guide shows how to test different pagination patterns in APIs. Perfect for validating pagination logic and performance under load.
Use Cases
- Test pagination performance under load
- Validate pagination logic and consistency
- Test different pagination patterns
- Check boundary conditions and edge cases
Simple Implementation
from locust import task, HttpUser
import random
import json
class PaginationTestUser(HttpUser):
def on_start(self):
# API endpoints with different pagination types
self.endpoints = {
"/api/users": "offset", # offset-based: ?limit=20&offset=40
"/api/products": "page", # page-based: ?page=2&per_page=25
"/api/orders": "cursor" # cursor-based: ?cursor=abc123&limit=30
}
@task(4)
def test_offset_pagination(self):
"""Test offset-based pagination (?limit=20&offset=40)"""
endpoint = "/api/users"
limit = 20
offset = random.choice([0, 20, 40, 60, 100])
params = {"limit": limit, "offset": offset}
with self.client.get(
endpoint,
params=params,
name=f"Offset Pagination - {offset}"
) as response:
if response.status_code == 200:
try:
data = response.json()
# Handle different response formats
if isinstance(data, dict) and "data" in data:
items = data["data"]
total = data.get("total", 0)
print(f"Offset pagination: {len(items)} items, offset {offset}")
# Basic validation
if len(items) > limit:
response.failure(f"Too many items: {len(items)} > {limit}")
else:
items = data if isinstance(data, list) else []
print(f"Offset pagination: {len(items)} items")
except json.JSONDecodeError:
response.failure("Invalid JSON response")
else:
response.failure(f"Pagination failed: {response.status_code}")
@task(3)
def test_page_pagination(self):
"""Test page-based pagination (?page=2&per_page=25)"""
endpoint = "/api/products"
per_page = 25
page = random.choice([1, 2, 3, 4, 5])
params = {"per_page": per_page, "page": page}
with self.client.get(
endpoint,
params=params,
name=f"Page Pagination - {page}"
) as response:
if response.status_code == 200:
try:
data = response.json()
if isinstance(data, dict):
items = data.get("data", data.get("items", []))
current_page = data.get("current_page", data.get("page", page))
total_pages = data.get("total_pages", 0)
print(f"Page pagination: {len(items)} items, page {current_page}")
# Validate page logic
if len(items) > per_page:
response.failure(f"Too many items per page: {len(items)}")
else:
items = data if isinstance(data, list) else []
print(f"Page pagination: {len(items)} items")
except json.JSONDecodeError:
response.failure("Invalid JSON response")
else:
response.failure(f"Page pagination failed: {response.status_code}")
@task(2)
def test_cursor_pagination(self):
"""Test cursor-based pagination (?cursor=abc123&limit=30)"""
endpoint = "/api/orders"
limit = 30
# Test with different cursors (in real use, get from previous responses)
cursor = random.choice([None, "start", "abc123", "xyz789"])
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
with self.client.get(
endpoint,
params=params,
name=f"Cursor Pagination - {cursor or 'start'}"
) as response:
if response.status_code == 200:
try:
data = response.json()
if isinstance(data, dict):
items = data.get("data", data.get("items", []))
next_cursor = data.get("next_cursor")
has_more = data.get("has_more", False)
print(f"Cursor pagination: {len(items)} items, next: {next_cursor}")
# Basic validation
if len(items) > limit:
response.failure(f"Too many items: {len(items)} > {limit}")
else:
items = data if isinstance(data, list) else []
print(f"Cursor pagination: {len(items)} items")
except json.JSONDecodeError:
response.failure("Invalid JSON response")
else:
response.failure(f"Cursor pagination failed: {response.status_code}")
@task(1)
def test_pagination_edge_cases(self):
"""Test pagination edge cases"""
endpoint = random.choice(list(self.endpoints.keys()))
pagination_type = self.endpoints[endpoint]
# Test edge cases based on pagination type
if pagination_type == "offset":
# Test large offset
params = {"limit": 10, "offset": 999999}
edge_case = "large_offset"
elif pagination_type == "page":
# Test page 0
params = {"per_page": 10, "page": 0}
edge_case = "page_zero"
else: # cursor
# Test with empty cursor
params = {"limit": 10, "cursor": ""}
edge_case = "empty_cursor"
with self.client.get(
endpoint,
params=params,
name=f"Edge Case - {edge_case}"
) as response:
if response.status_code in [200, 400, 404]:
print(f"Edge case {edge_case}: {response.status_code}")
else:
print(f"Edge case {edge_case}: unexpected {response.status_code}")
Setup Instructions
- Replace endpoint URLs with your actual API endpoints
- Adjust pagination parameters to match your API's format
- Update field names if your API uses different response structure
- Customize limit/per_page values based on your API's defaults
What This Tests
- Offset Pagination: Tests limit/offset based pagination
- Page Pagination: Tests page number and per_page based pagination
- Cursor Pagination: Tests cursor-based pagination for large datasets
- Edge Cases: Tests boundary conditions and invalid parameters
Best Practices
- Test different page sizes to find optimal performance
- Validate response structure consistency across pages
- Monitor response times for different pagination positions
- Test edge cases like empty results and invalid parameters
Common Issues
- Inconsistent Response Format: Ensure all pages return same structure
- Performance Degradation: Later pages may be slower (especially offset-based)
- Boundary Conditions: Handle edge cases like page 0 or negative offsets
- Cursor Validity: Cursor-based pagination requires valid cursor tokens