How to Write Test Cases for Pagination (With Examples)
How to Write Test Cases for Pagination (With Examples) is a practical guide that walks you through the anatomy of a pagination test case, shows you how to derive positive, negative, boundary, and edge
How to Write Test Cases for Pagination (With Examples) is a practical guide that walks you through the anatomy of a pagination test case, shows you how to derive positive, negative, boundary, and edge‑case scenarios, and ties those cases to requirements and autonomous exploration. The goal is to give you a reusable test matrix you can execute manually or automate, plus a short checklist you can bookmark for future pagination features.
Understanding Pagination Mechanics
Pagination splits a large result set into smaller, consumable chunks. Whether the data comes from a relational database, a NoSQL store, or a REST API, the core concepts are the same:
- Page size – number of items returned per request (often called
limit,pageSize, orsize). - Cursor or offset – the starting point for the next chunk (
offset,page,startIndex, or a token). - Total count – optional field that tells the client how many items exist overall (
total,totalElements,recordsTotal). - Navigation controls – UI elements such as “Previous”, “Next”, page numbers, or infinite‑scroll triggers.
When a client requests page N, the service typically computes skip = (N‑1) * pageSize and returns items skip … skip+pageSize‑1. Edge conditions arise when:
skipexceeds the total count → empty page.pageSizeis zero, negative, or larger than the total count.- The cursor token is malformed, expired, or tampered with.
- The underlying data changes between pages (inserts/deletes).
Understanding these mechanics lets you map each variable to a test condition and predict the exact system behavior.
Test Case Anatomy for Pagination
A well‑structured test case contains five essential parts:
| Element | Description | Example for pagination |
|---|---|---|
| ID | Unique identifier (e.g., PG-01) | PG-01 |
| Preconditions | State that must exist before execution | 200 items in DB, page size = 10, user logged in |
| Steps | Ordered actions the tester or automation performs | 1. GET /items?page=1&size=10 2. Verify response contains items 1‑10 |
| Expected Result | Observable outcome that determines PASS/FAIL | Response status 200, body includes total=200, page=1, size=10, items array length = 10, next link present |
| Post‑conditions (optional) | State left after the test (useful for chaining) | DB unchanged, no side‑effects |
Keep each step atomic and avoid bundling multiple assertions into a single step; this makes failure diagnosis faster. When writing automated scripts, map each step to a function or keyword (e.g., Playwright’s await page.goto(url)).
How to Write Test Cases for Pagination (With Examples): Positive and Boundary Cases
Positive cases verify that the system behaves correctly when inputs are within the declared contract. Boundary cases push those inputs to the limits of the contract (minimum, maximum, just‑inside, just‑outside). Below is a table of 22 representative cases. Feel free to copy the IDs into your test management tool and adjust the preconditions to match your domain.
Test Matrix (Positive & Boundary)
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PG-01 | 0 items in collection, pageSize = 10 | GET /items?page=1&size=10 | 200 OK, total=0, items=[], no next link |
| PG-02 | 5 items, pageSize = 10 | GET /items?page=1&size=10 | 200 OK, total=5, items length = 5, no next |
| PG-03 | 10 items, pageSize = 10 | GET /items?page=1&size=10 | 200 OK, total=10, items length = 10, no next |
| PG-04 | 11 items, pageSize = 10 | GET /items?page=1&size=10 | 200 OK, total=11, items length = 10, next link present |
| PG-05 | 100 items, pageSize = 10 | GET /items?page=10&size=10 | 200 OK, total=100, items items 91‑100, next absent, prev present |
| PG-06 | 100 items, pageSize = 10 | GET /items?page=11&size=10 | 200 OK, total=100, items=[], next absent, prev present |
| PG-07 | 100 items, pageSize = 1 | GET /items?page=50&size=1 | 200 OK, total=100, single item #50, next & prev present |
| PG-08 | 100 items, pageSize = 100 | GET /items?page=1&size=100 | 200 OK, total=100, items length = 100, no navigation links |
| PG-09 | 100 items, pageSize = 101 | GET /items?page=1&size=101 | 200 OK (or 400 if server rejects oversized size), total=100, items length = 100, no next |
| PG-10 | 100 items, pageSize = 0 | GET /items?page=1&size=0 | 200 OK, total=100, items=[], pagination links may be hidden or disabled |
| PG-11 | 100 items, pageSize = -5 | GET /items?page=1&size=-5 | 400 Bad Request (validation error) |
| PG-12 | 100 items, page = 0 | GET /items?page=0&size=10 | 400 Bad Request (page numbers start at 1) |
| PG-13 | 100 items, page = -1 | GET /items?page=-1&size=10 | 400 Bad Request |
| PG-14 | 100 items, page = 1000000 | GET /items?page=1000000&size=10 | 200 OK, total=100, items=[], navigation disabled |
| PG-15 | 100 items, cursor token based (e.g., base64 of offset) | Request first page, extract next token, request using token | 200 OK, returns items 11‑20, token updated correctly |
| PG-16 | 100 items, cursor token expired | Use token from a previous run after DB reset | 401/400 (token invalid) or fallback to first page |
| PG-17 | 100 items, token tampered (flip a bit) | Send altered token | 400 Bad Request |
| PG-18 | 100 items, mixed sort order (ASC/DESC) | GET /items?page=2&size=10&sort=name,asc then desc | Items appear in correct order, no duplication/gaps |
| PG-19 | 100 items, filter applied (status=active) | GET /items?page=1&size=10&status=active | Only active items returned, pagination respects filtered total |
| PG-20 | 100 items, multiple filters (status=active&type=premium) | GET with both query params | Pagination works on intersected result set |
| PG-21 | 100 items, request includes unwanted params (page=1&size=10&debug=true) | Send request with extra param | Server ignores unknown param, returns correct page |
| PG-22 | 100 items, large pageSize (e.g., 1000) | GET /items?page=1&size=1000 | 200 OK, returns all 100 items, total=100, no next |
Each case isolates a single variable (page number, page size, cursor, sort, filter) while holding others constant. This makes it easy to trace a failure back to a specific requirement, such as “page size must be a positive integer ≤ maxPageSize”.
How to Write Test Cases for Pagination (With Examples): Negative and Error Cases
Negative cases probe how the system reacts when inputs violate the contract or when unexpected conditions arise. They are crucial for guarding against crashes, information leakage, or poor UX.
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PGN-01 | 100 items, pageSize = 10 | GET /items?page=abc&size=10 | 400 Bad Request, validation message for non‑numeric page |
| PGN-02 | 100 items, pageSize = 10 | GET /items?page=1&size=xyz | 400 Bad Request, validation message for non‑numeric size |
| PGN-03 | 100 items, pageSize = 10 | GET /items?page=1&size=10&page=2 (duplicate param) | Either 400 or uses the last value; document which behavior is expected |
| PGN-04 | 100 items, pageSize = 10 | GET /items?page=1&size=10 with invalid Authorization header | 401 Unauthorized |
| PGN-05 | 100 items, pageSize = 10 | GET /items?page=1&size=10 after DB connection loss | 503 Service Unavailable or retry‑after header |
| PGN-06 | 100 items, pageSize = 10 | GET /items?page=1&size=10 with payload size > limit (e.g., oversized JWT) | 413 Payload Too Large or 400 |
| PGN-07 | 100 items, pageSize = 10 | GET /items?page=1&size=10 while requesting a non‑existent sort field | 400 Bad Request, message indicating unknown sort |
| PGN-08 | 100 items, pageSize = 10 | GET /items?page=1&size=10 with a range header that conflicts with pagination | 416 Range Not Satisfiable (if server implements RFC 7233) |
| PGN-09 | 100 items, pageSize = 10 | Simulate network latency (e.g., tc netem) and abort request mid‑stream | Client receives a network error; server logs no partial response |
| PGN-10 | 100 items, pageSize = 10 | Send request with page=1&size=10&_method=PUT (method tampering) | 405 Method Not Allowed |
| PGN-11 | 100 items, pageSize = 10 | Request with page=1&size=10 while another process deletes the last item between page 9 and page 10 | Page 10 may be empty or contain shifted items; verify that the API does not throw 500 |
| PGN-12 | 100 items, pageSize = 10 | Request with page=1&size=10 and an illegal character in the token (e.g., null byte) | 400 Bad Request |
| PGN-13 | 100 items, pageSize = 10 | Request with page=1&size=10 and Accept: application/xml when only JSON is supported | 406 Not Acceptable |
| PGN-14 | 100 items, pageSize = 10 | Request with page=1&size=10 and If-None-Match matching an ETag for a different page | 304 Not Modified (if caching implemented) or 200 with correct page |
| PGN-15 | 100 items, pageSize = 10 | Send a huge number of pagination requests in a short burst (rate‑limit test) | After threshold, receive 429 Too Many Requests with retry‑after |
| PGN-16 | 100 items, pageSize = 10 | GET /items?page=1&size=10 while the server is configured with a maxPageSize of 50 and request asks for size=100 | 400 Bad Request or server caps size to max and returns adjusted page (document which) |
| PGN-17 | 100 items, pageSize = 10 | Request with page=1&size=10 and a future timestamp in If-Modified-Since header | 200 OK (or 304 if resource not modified) – verify server ignores future dates |
| PGN-18 | 100 items, pageSize = 10 | Request with page=1&size=10 and Connection: close header | Response includes Connection: close and socket closes after body |
| PGN-19 | 100 items, pageSize = 10 | Request with page=1&size=10 and Upgrade: websocket | 400 Bad Request (or 426 Upgrade Required) |
| PGN-20 | 100 items, pageSize = 10 | Request with page=1&size=10 and a malicious SQL injection attempt in sort parameter | 400 Bad Request; no SQL error leaked in response body |
These cases ensure that validation, error handling, security, and resilience mechanisms are in place. When automating, assert on HTTP status codes, response headers, and that error messages do not contain stack traces or internal identifiers.
How to Write Test Cases for Pagination (With Examples): Edge Cases and Production‑Only Scenarios
Some defects only manifest under load, with real‑world data distributions, or when external systems behave unexpectedly. Capture them in your test plan even if they are hard to reproduce in a clean test environment.
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PGE-01 | 10 million items, pageSize = 100 | GET /items?page=50000&size=100 (deep page) | Response within SLA (e.g., < 2 s), correct items 4 999 901‑5 000 000 |
| PGE-02 | 10 million items, pageSize = 100 | Repeatedly request pages 1‑100 in rapid succession | No memory leak; steady CPU usage; each page returns correct slice |
| PGE-03 | Items have variable length strings (average 2 KB, some 1 MB) | GET /items?page=1&size=10 | Payload size reflects actual data; no truncation or corruption |
| PGE-04 | Items contain Unicode emojis and RTL characters | GET /items?page=1&size=10 | Characters render correctly in UI; no encoding errors |
| PGE-05 | Items have null values in sortable column | GET /items?page=1&size=10&sort=nullableField,asc | Nulls appear consistently (either first or last per DB collation) |
| PGE-06 | Items have duplicate sort keys | GET /items?page=1&size=10&sort=nonUniqueField,asc | Stable ordering: same duplicates appear in same relative order across pages |
| PGE-07 | Real‑time feed: new items inserted while paging | Request page 1, wait 5 s, request page 2 | Page 2 reflects the state at the time of its request; no duplication or missing items caused by intervening inserts |
| PGE-08 | Items soft‑deleted (flag isDeleted=true) | GET /items?page=1&size=10&includeDeleted=false | Only non‑deleted items returned; pagination count excludes soft‑deleted rows |
| PGE-09 | Items hard‑deleted between page 1 and page 2 requests | Request page 1, delete last item of page 1, request page 2 | Page 2 shows the next item after the deleted one; total count decreased by 1 |
| PGE-10 | Mixed read/write workload (other service updates same table) | Run a background job that updates 100 random rows per second while paginating | No dirty reads; each page shows a consistent snapshot (if using repeatable read or MVCC) |
| PGE-11 | API behind a CDN that caches based on query string | Request page 1, then page 2, then page 1 again | Second request for page 1 returns cached copy (if allowed) with correct data; ensure stale‑while‑revalidate behavior matches contract |
| PGE-12 | Mobile client on high latency (300 ms RTT) and lossy link (5 % packet loss) | Use tc to emulate network, perform infinite‑scroll | UI shows loading spinner, retries on failure, eventually displays correct items |
| PGE-13 | Screen‑reader user navigating pagination controls | Use VoiceOver/TalkBack to move focus to “Next” button after last page | Button is announced as disabled; no unexpected focus shift |
| PGE-14 | User with motor impairments uses switch device to activate “Next” | Simulate switch activation | Activation works, no double‑trigger due to bounce |
| PGE-15 | Power user appends custom pageSize=9999 via devtools | Attempt to set size via UI override | Server rejects or caps to maxPageSize; UI reflects the capped value |
| PGE-16 | Adversarial user sends page=-9223372036854775808 (min int64) | GET with that value | 400 Bad Request; no overflow or server crash |
| PGE-17 | Request includes a huge number of pagination parameters (page=1&size=10&page=2&size=20…) | Send with many duplicates | Server either uses last occurrence or returns 400; behavior must be documented and consistent |
| PGE-18 | API versioning: client calls v1 endpoint that uses offset pagination while server migrated to cursor pagination | Call v1 endpoint | Either returns 410 Gone or provides shim that translates offset to cursor; ensure no 500 |
| PGE-19 | Server returns total as a floating‑point number due to ORM bug | GET any page | Client detects type mismatch; either server fixes or client safely casts to integer |
| PGE-20 | Client mistakenly sends page=1.5 (non‑integer float) | GET with float | 400 Bad Request; no silent coercion to 1 or 2 |
These edge cases often surface only after the system has been in production for weeks or months, especially when data volume grows or when third‑party networks fluctuate. Documenting them helps you prioritize load‑testing, chaos‑engineering, and accessibility validation.
Prioritization, Traceability, and Data Setup
Not all test cases carry the same risk. Use a simple risk‑based matrix that combines impact (how severe a failure would be) and likelihood (how often the condition occurs in production).
| Priority | Impact | Likelihood | Example IDs |
|---|---|---|---|
| P0 | Crash, data corruption, security breach | High | PGN-01, PGN-04, PGN-11, PGE-08, PGE-09 |
| P1 | Functional defect (wrong page, missing items) | Medium | PG-02, PG-04, PG-07, PG-15, PGE-01, PGE-03 |
| P2 | UI glitch, performance degradation, minor error message | Low | PGN-09, PGN-12, PGN-18, PGE-11, PGE-14 |
| P3 | Cosmetic, documentation mismatch | Very low | PGN-15, PGN-19, PGE-16 |
Traceability – Map each test case to a requirement ID from your specification (e.g., `REQ-PAG-001: “The API shall return exactly pageSize items unless fewer remain.”). Keep a traceability matrix in your test management tool or a simple spreadsheet:
| Requirement | Verified By |
|---|---|
| REQ-PAG-001 | PG-01, PG-02, PG-03, PG-04, PG-05, PG-06, PG-07, PG-08, PG-09, PG-10 |
| REQ-PAG-002 | PGN-01, PGN-02, PGN-03, PGN-11 |
| REQ-PAG-003 | PG-15, PG-16, PG-17 |
| REQ-PAG-004 | PGE-01, PGE-02, PGE-03 |
| REQ-PAG-005 | PGN-04, PGN-05, PGN-06 |
| REQ-PAG-006 | PGE-08, PGE-09 |
| REQ-PAG-007 | PGN-13, PGN-14 |
| REQ-PAG-008 | PGE-11, PGE-12 |
| REQ-PAG-009 | PGN-16, PGN-17 |
| REQ-PAG-010 | PGE-13, PGE-14, PGE-15 |
Data Setup – Automate the creation of the baseline dataset using scripts or Docker containers. For a relational database you might use:
-- create_base_data.sql
TRUNCATE TABLE items;
INSERT INTO items (id, name, status, type, created_at)
SELECT
generate_series(1,1000000) AS id,
'Item-' || generate_series(1,1000000) AS name,
CASE WHEN (random() * 100) < 20 THEN 'inactive' ELSE 'active' END AS status,
CASE WHEN (random() * 100) < 10 THEN 'premium' ELSE 'standard' END AS type,
now() - (random() * interval '365 days') AS created_at
FROM generate_series(1,1000000);
For NoSQL or document stores, use a bulk import tool (e.g., mongoimport with a JSON array). After each test run that mutates state (delete, update), either roll back a transaction or reseed the database from a known snapshot. Keep the seed script under version control so every environment (dev, CI, staging) starts identical.
Manual vs Automated Execution Strategies
Manual Execution
- When to use – Exploratory testing, usability checks, accessibility validation, and ad‑hoc verification of edge cases that are hard to automate (e.g., screen‑reader navigation).
- How to execute – Follow the steps column verbatim, record observations in a test log, and capture screenshots or video for failures. Use a checklist (see later) to ensure you don’t skip preconditions like data reset.
- Tools – Browser dev tools for network inspection, Postman or Insomnia for API calls, accessibility auditors (axe, Lighthouse), and device labs for real‑device interaction.
Automated Execution
- When to use – Regression suites, CI pipelines, performance monitoring, and any case that can be expressed as a deterministic request/response interaction.
- API Layer – Use a language‑agnostic runner such as k6, Locust, or JMeter for load; for functional verification, use REST‑Assured (Java), pytest‑requests (Python), or SuperTest (Node.js). Example in Python:
import requests
def get_page(base_url, page, size):
resp = requests.get(
f"{base_url}/items",
params={"page": page, "size": size},
headers={"Accept": "application/json"},
timeout=5,
)
resp.raise_for_status()
return resp.json()
# Positive case PG-04
data = get_page("https://api.example.com", page=1, size=10)
assert data["total"] == 11
assert len(data["items"]) == 10
assert data["links"]["next"] is not None
assert data["links"]["prev"] is None
- UI Layer – For web applications, Playwright or Cypress can assert pagination controls, network responses, and DOM updates. Example Playwright snippet:
from playwright.sync_api import expect, sync_playwright
def test_next_button_disabled_on_last_page():
with sync_playwright() as p:
page = p.chromium.launch(headless=False).new_page()
page.goto("https://app.example.com/items")
# set page size to 10 via UI
page.select_option("select#page-size", "10")
# navigate to last page (calculate from total count)
total = int(page.inner_text("span#total-count"))
page_size = 10
last_page = (total + page_size - 1) // page_size
page.goto(f"?page={last_page}&size={page_size}")
expect(page.locator("button#next")).to_be_disabled()
- Mobile Layer – For Android apps, Appium with Java or Kotlin can verify scroll‑based pagination. Example:
@Test
public void testInfiniteScrollLoadsMoreItems() {
AndroidDriver<MobileElement> driver = getDriver();
// Wait for initial list
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
By.id("item_row"), 0));
int initialCount = driver.findElements(By.id("item_row")).size();
// Scroll to bottom
MobileElement list = driver.findElement(By.id("recycler_view"));
((JavascriptExecutor) driver).executeScript(
"mobile: swipe", ImmutableMap.of(
"element", list.getId(),
"direction", "down",
"percent", 0.9));
// Wait for new items
wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(
By.id("item_row"), initialCount));
assertTrue(driver.findElements(By.id("item_row")).size() > initialCount);
}
- Assertions – Besides status codes, validate:
totalmatches the count of items in the database (or a derived count).- No duplicate IDs appear across consecutive pages.
- Sorting order is monotonic according to the requested sort key.
- Links (
next,prev) are present exactly when appropriate. - Response time stays within SLA (use a timer assertion).
- CI Integration – Add the test suite to your pipeline with a stage that runs on every pull request. Use containerized test agents (e.g.,
docker run --rm -v $(pwd):/tests susatest-agent run --suite pagination) to guarantee environment parity.
Leveraging Autonomous Exploration (SUSA) for Pagination Coverage
SUSA explores an application without pre‑written scripts, generating real user interactions and capturing any anomalies. When you point SUSA at a paginated list, it will:
- Discover navigation controls – It taps “Next”, “Previous”, page numbers, and infinite‑scroll triggers, learning which actions produce new content.
- Vary personas – The curious persona may keep tapping “Next” until it hits the end; the impatient persona may jump to a high page number via URL manipulation; the accessibility persona will use screen‑reader gestures to verify announcements.
- Detect regressions – If a recent change disables the “Next” button on the last page, SUSA will record a failure because the curious‑end state is missing.
- Generate regression scripts – After a run, SUSA outputs Appium (Android) and Playwright (Web) scripts that reproduce the exact taps, scrolls, and inputs it performed. You can add these to your CI suite as a safety net.
How to invoke SUSA for pagination testing
# Install the agent (once)
pip install susatest-agent
# Point at a web URL; SUSA will crawl and paginate
susatest-agent run \
--url https://shop.example.com/products \
--personas curious impatient accessibility \
--output-dir ./susausage-report \
--generate-scripts
The --personas flag tells SUSA
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free