Resource library

QA How-To

Testing pagination and filtering (2026)

Learn testing pagination and filtering with boundary cases, API and UI examples, stable data setup, cursor checks, and an interview-ready QA strategy.

20 min read | 3,368 words

TL;DR

Testing pagination and filtering requires deterministic data, explicit contract assertions, and checks across page boundaries. Prove coverage, uniqueness, order, filter correctness, navigation behavior, and resilience to data changes rather than checking only the first page.

Key Takeaways

  • Treat pagination metadata, item order, and navigation links as one contract, not separate assertions.
  • Seed deterministic records so exact boundaries, duplicates, gaps, and filter matches can be proved.
  • Test offset and cursor pagination differently because their consistency risks are not the same.
  • Validate filters across equivalence classes, invalid syntax, normalization rules, and authorization scope.
  • Combine filters, sort order, and page navigation to expose state loss and unstable ordering.
  • Compare API truth with UI behavior while keeping most combinatorial coverage below the browser layer.

Testing pagination and filtering means proving that a client can traverse the intended result set without missing, duplicating, leaking, or misordering records, while every active filter remains correct. A strong test strategy covers API contracts, database truth, UI state, boundaries, concurrency, accessibility, and performance.

The difficult bugs rarely appear on page one with the default settings. They appear when the last page is partial, several records share a sort value, data changes between requests, a user clears one of three filters, or a cursor is reused with different criteria. This guide turns those risks into a repeatable QA approach with runnable TypeScript, SQL, and Playwright examples.

TL;DR

Area Minimum proof High-value failure to target
Page contract Items, limit, continuation metadata, stable order Incorrect hasNext, count, or next link
Coverage Union of pages equals the expected result set Missing or duplicated boundary record
Filters Every returned item satisfies every active predicate UI label changes but request does not
Combined state Sort, filter, and pagination parameters persist Page change silently drops a filter
Mutation Documented consistency model is honored Offset shifts after an insert
UI Controls, URL state, focus, loading, and empty views work Stale response overwrites newer results

Start with deterministic fixtures and an oracle for the complete expected set. Assert invariants across all pages, then add focused UI checks for integration behavior.

1. Testing Pagination and Filtering Starts With the Contract

Before writing test cases, describe what the endpoint or screen promises. Offset pagination commonly accepts page and limit, or offset and limit. Cursor pagination accepts an opaque continuation token. Filtering may use individual query parameters, a compact expression language, or a JSON request body. None of these choices is automatically correct, but each needs an unambiguous contract.

Record the default page size, maximum page size, first-page representation, behavior beyond the last page, and whether totals are exact, approximate, or absent. Define the stable sort order, including a unique tie-breaker. created_at DESC is not deterministic when two rows share a timestamp, while created_at DESC, id DESC can be. Also document whether a next link is relative or absolute and whether it preserves encoded filter values.

For filters, identify data types, supported operators, case sensitivity, Unicode normalization, time zone interpretation, null semantics, repeated parameter rules, and unknown-field behavior. A blank value might mean no filter, an empty string, or invalid input. QA should not guess. Turn each decision into an assertion or a product question before release.

The response contract deserves equal attention. Verify item schema, metadata types, link relations, error format, and authorization boundaries. If a total count is included, decide whether it represents all accessible records after filtering. The guide on API pagination testing provides additional endpoint patterns, while this article concentrates on the combined behavior of pagination, filtering, and UI state.

2. Build a Risk-Based Pagination Test Matrix

Use equivalence partitions instead of multiplying every value mechanically. For a page size of 20, useful dataset sizes are 0, 1, 19, 20, 21, 40, and 41. They represent empty, singleton, partial, exact, first-overflow, exact-multiple, and partial-final-page states. Repeat only the highest-risk partitions for alternative page sizes.

A compact matrix should cover positive, negative, boundary, state, and authorization cases:

Dimension Representative cases Main assertions
Dataset size 0, 1, limit minus 1, limit, limit plus 1, multiple plus 1 Page count, partial page, navigation state
Page input Missing, first, middle, last, beyond last, zero, negative, text, huge value Defaulting or defined error response
Limit input Minimum, default, maximum, above maximum, decimal, repeated Clamp, reject, and metadata consistency
Sort values Unique, ties, nulls, mixed case Deterministic sequence and tie-breaker
Filter result size None, one, exact page, several pages Predicate truth and count
User scope Owner, other tenant, admin, anonymous No cross-scope totals or records

Add pairwise combinations around meaningful interactions. For example, test a date range that yields exactly 20 records, a status filter yielding 21, and a search string yielding no results on the current page but matches on page one. If the product resets to page one when a filter changes, verify that explicitly.

Prioritize by consequence. Duplicate catalog products may be annoying, while a repeated financial transaction or leaked tenant record is severe. This framing keeps the suite small enough to maintain and strong enough to catch business failures. Maintain traceability from each contract statement to at least one automated assertion and one suitable test layer.

3. Create Deterministic Data and a Reliable Oracle

Random production-like data is valuable for exploration, but exact pagination checks need known records. Seed values that force boundary conditions and tie situations. Keep identifiers and timestamps deterministic, and clean up by a run-specific marker so parallel builds do not collide.

The following PostgreSQL setup creates 41 orders, enough for two full pages and one item at a limit of 20. Several rows share a timestamp, so the API must use id as a secondary sort key. Run it inside a disposable test database or a transaction controlled by the suite.

CREATE TEMP TABLE qa_orders (
  id integer PRIMARY KEY,
  tenant_id integer NOT NULL,
  status text NOT NULL CHECK (status IN ('NEW', 'PAID', 'CANCELLED')),
  total_cents integer NOT NULL,
  created_at timestamptz NOT NULL
);

INSERT INTO qa_orders (id, tenant_id, status, total_cents, created_at)
SELECT
  n,
  CASE WHEN n = 41 THEN 200 ELSE 100 END,
  CASE WHEN n % 3 = 0 THEN 'PAID' ELSE 'NEW' END,
  1000 + n,
  TIMESTAMPTZ '2026-07-13 10:00:00+00' + ((n / 4) * INTERVAL '1 minute')
FROM generate_series(1, 41) AS n;

Build the expected result with the same documented predicates, but avoid copying application implementation code into the test. A database query, purpose-built fixture model, or independently computed array can be the oracle. For tenant 100 and status=PAID, query WHERE tenant_id = 100 AND status = 'PAID' ORDER BY created_at DESC, id DESC.

Never assert only items.length <= limit. That can pass when the service returns arbitrary records. Assert exact IDs for controlled fixtures, and assert general invariants such as uniqueness, monotonic order, predicate truth, and scope for broader datasets. The SQL for test data setup and teardown guide explains isolation patterns that keep these tests repeatable.

4. Automate Offset Pagination Without Hiding Defects

Offset pagination is easy to call and easy to test incompletely. A traversal helper should fail on a non-success response, validate the response shape, guard against infinite loops, and preserve evidence for each page. It must not silently deduplicate items because deduplication would hide the exact defect the test is meant to expose.

This Playwright API test uses the built-in request fixture and standard assertions. It assumes a response shaped as { items, page, limit, total, hasNext }. Adapt field names to the real contract.

import { test, expect } from '@playwright/test';

type Order = {
  id: number;
  status: 'NEW' | 'PAID' | 'CANCELLED';
  createdAt: string;
};

type Page = {
  items: Order[];
  page: number;
  limit: number;
  total: number;
  hasNext: boolean;
};

test('all filtered offset pages are complete, unique, and ordered', async ({ request }) => {
  const all: Order[] = [];
  const limit = 20;

  for (let page = 1; page <= 100; page += 1) {
    const response = await request.get('/api/orders', {
      params: { page, limit, status: 'PAID', sort: '-createdAt,-id' }
    });
    expect(response.ok(), await response.text()).toBeTruthy();

    const body = (await response.json()) as Page;
    expect(body.page).toBe(page);
    expect(body.limit).toBe(limit);
    expect(body.items.length).toBeLessThanOrEqual(limit);
    expect(body.items.every(order => order.status === 'PAID')).toBe(true);
    all.push(...body.items);

    if (!body.hasNext) {
      expect(body.items.length).toBeLessThanOrEqual(limit);
      expect(all).toHaveLength(body.total);
      break;
    }
    expect(body.items).toHaveLength(limit);
  }

  expect(new Set(all.map(order => order.id)).size).toBe(all.length);
  const keys = all.map(order => `${order.createdAt}|${String(order.id).padStart(10, '0')}`);
  expect(keys).toEqual([...keys].sort().reverse());
});

Also request the page after the declared end. The accepted result may be an empty list or a defined 4xx error, but it must match the contract and avoid an expensive unbounded scan. Test offset overflow, negative values, decimals, repeated query keys, and limits above the cap. Verify that malformed inputs produce a stable client error rather than a server exception.

5. Test Cursor Pagination as an Opaque Continuation Protocol

A cursor is not just another page number. It represents continuation from a position under a particular sort and filter context. Clients should treat it as opaque, and tests should focus on its behavior rather than decoding its internal content. Assert that a nonempty next cursor appears only when more accessible matches exist, that following it advances without overlap, and that the last response omits or nulls the continuation token as documented.

Use the exact cursor string returned by the server. Verify URL encoding by choosing filters with spaces, plus signs, ampersands, and non-ASCII characters. A cursor should normally be rejected when corrupted. The API must also define what happens when a valid cursor is replayed, expires, belongs to another tenant, or is combined with a different sort or filter. Safe designs bind continuation state to these criteria or reject inconsistent combinations.

Concurrency tests distinguish cursor pagination from offset behavior. Fetch page one, insert a record that sorts before its first item, then fetch page two. With a correctly implemented keyset cursor, the new earlier record should not cause an existing record to repeat. Next, delete an item after the cursor and confirm traversal continues coherently. Do not demand snapshot isolation unless the product promises it. Instead, verify the documented consistency model.

Security matters too. Tampering with an encoded token must not expand tenant scope or expose database values through detailed parsing errors. Do not log raw tokens if they embed sensitive state. Run the same traversal with two identities and prove a cursor from tenant A cannot be used to retrieve tenant B data.

6. Cover Filter Semantics, Validation, and Encoding

Filter testing begins with predicate truth: every returned item must satisfy the active filter. It continues with completeness: every accessible item that should match must be obtainable across the pages. Test both because an endpoint can return only valid matches while silently omitting half of them.

Partition by type. Strings need exact, contains, prefix, case, whitespace, Unicode, reserved URL characters, and empty-value cases. Numbers need zero, negative, decimal, boundaries, and numeric strings. Dates need inclusive and exclusive endpoints, time zones, daylight-saving transitions where applicable, invalid calendar dates, and start-after-end ranges. Enumerations need every allowed value, wrong case, unknown values, and repeated values. Boolean fields need true, false, missing, and null when nullable.

For multiple filters, establish whether predicates use AND or OR. A request such as status=PAID&minTotal=5000 usually means both constraints, but repeated status=PAID&status=NEW may mean OR, last-value-wins, or invalid. Verify the documented interpretation at API and UI layers. Check that clearing one chip removes only its query parameter.

Treat error behavior as part of the public API. Unknown field names and unsupported operators should not be silently ignored if that could mislead users. Assert a stable status code, machine-readable error code, field location, and safe message. For a broader foundation in request design and assertions, use the API security testing basics guide. It is especially relevant when filter languages could expose unintended fields or expensive queries.

Finally, compare percent-encoded requests rather than hand-building URLs. Client parameter APIs prevent common mistakes with &, +, #, and Unicode. Still inspect the network request in one integration test to prove that UI values map to the expected API representation.

7. Testing Pagination and Filtering Together

The highest-value scenarios combine state. Apply a filter that yields several pages, move to page two, change the sort, remove one filter, use Back, refresh, and share the URL in a new context. At every transition, assert the defined page reset, active criteria, visible result set, and request parameters.

Page reset behavior deserves an explicit rule. If a user is on page five and narrows results to one page, retaining page five may show a misleading empty screen. Most interfaces reset to page one whenever result membership or order changes. If only a display preference changes, retaining the current page may be acceptable. Write tests against the product rule, not personal preference.

For each combined test, compute an expected ordered list first. Split it into pages using the declared limit, then compare exact IDs page by page. This catches four defect classes at once: wrong predicate, wrong order, missing boundary record, and incorrect metadata. Add duplicate checks across the concatenated sequence rather than within each page only.

Race conditions need deliberate simulation. A slow response for an old filter must not overwrite a faster response for the latest filter. In a browser test, intercept the old request, change the filter, allow the new response to render, then release the old response and assert the UI still shows the new state. Also test rapid Next clicks. Controls should disable, queue safely, or cancel obsolete requests so the page cannot jump unpredictably.

Cache keys must include normalized filter, sort, scope, page size, and continuation state. Execute two near-identical requests that differ in one filter or identity and prove the second response is not reused incorrectly. This is a crucial data-isolation check, not merely a performance check.

8. Validate the Browser Experience With Playwright

Keep combinatorial predicate tests at the API layer, then use a focused browser suite to validate wiring and user behavior. Cover initial defaults, one multi-page happy path, filter application and clearing, zero results, invalid server response, loading state, keyboard operation, URL persistence, and stale-response handling.

This example checks that an accessible table moves to page two and keeps the selected status in both the request and URL. It uses current Playwright locator and response APIs.

import { test, expect } from '@playwright/test';

test('keeps the status filter while moving to page two', async ({ page }) => {
  await page.goto('/orders');

  await page.getByLabel('Status').selectOption('PAID');
  const filtered = await page.waitForResponse(response => {
    const url = new URL(response.url());
    return url.pathname === '/api/orders' && url.searchParams.get('status') === 'PAID';
  });
  expect(filtered.ok()).toBeTruthy();

  await expect(page.getByRole('table')).toBeVisible();
  await expect(page.getByText('Status: Paid')).toBeVisible();

  const secondPageResponse = page.waitForResponse(response => {
    const url = new URL(response.url());
    return url.pathname === '/api/orders'
      && url.searchParams.get('status') === 'PAID'
      && url.searchParams.get('page') === '2';
  });
  await page.getByRole('button', { name: 'Next page' }).click();
  expect((await secondPageResponse).ok()).toBeTruthy();

  await expect(page).toHaveURL(/status=PAID/);
  await expect(page).toHaveURL(/page=2/);
  await expect(page.getByRole('button', { name: 'Previous page' })).toBeEnabled();
});

Prefer roles and labels over brittle CSS selectors. Assert that focus moves appropriately after navigation, the current page is announced, buttons expose disabled state, and a busy indicator does not trap keyboard users. If infinite scroll is used, verify a manual alternative or accessible load-more control, restoration after Back navigation, end-of-list messaging, and absence of repeated records.

9. Check Performance, Accessibility, and Observability

Correct results can still produce a poor product if deep pages time out, filters trigger full scans, or assistive technology cannot operate the controls. Establish service-level expectations with the team and measure representative first, middle, deep, empty, and highly selective queries. Do not encode arbitrary universal thresholds. Base gates on product objectives and a production-like dataset.

Compare response time as offsets grow. Large offsets can require the database to scan and discard many rows, even when only a small page is returned. Cursor pagination often scales more predictably when supported by a matching composite index. Use an execution plan in a safe environment to confirm that common filter and sort combinations use intended indexes. Never turn a unit test into a fragile query-plan snapshot, but retain performance evidence for risky endpoints.

Accessibility checks should include accessible names for pagination controls, a programmatic current-page indicator, keyboard reachability, visible focus, understandable empty results, and announcements when content updates without a page load. Zoom and narrow viewport testing can reveal clipped filters or unreachable controls.

Observability should answer which normalized query shape is slow without recording secrets or personal data. Capture route, duration, result count, page strategy, and a safe filter fingerprint. Monitor 4xx validation trends, 5xx rates, timeouts, maximum-offset usage, and cursor failures. Correlate frontend and API requests with a trace or request ID. This evidence shortens triage when a bug appears only under a particular sequence.

10. Organize Regression Coverage and CI Evidence

A maintainable suite separates pure predicate checks, API contract tests, database integration tests, browser flows, and targeted performance runs. Pure functions can verify sorting and expected-page partitioning quickly. API tests own the broad matrix. Browser tests prove integration points. Performance and concurrency checks may run on a scheduled environment with controlled data.

Use parameterized cases, but name each case by behavior. A failure called limit=21 is less useful than one record over default limit exposes next page. Preserve request parameters, returned IDs, expected IDs, page metadata, and correlation identifiers in test reports. Attach traces only where they help, because indiscriminate artifacts create noise and may retain sensitive records.

Run a small blocking suite on each pull request: empty results, exact boundary, boundary plus one, one compound filter, invalid input, tenant isolation, and one UI state flow. Schedule the larger pairwise, mutation, deep-pagination, accessibility, and load suites. Re-run contract tests against deployed environments using dedicated accounts and known safe fixtures.

Flakiness is usually a data-isolation or synchronization problem, not a reason to add sleeps. Give every run a unique data marker, await observable responses or UI states, and freeze time where date filters depend on the current instant. If shared test data must be used, assert invariant properties rather than exact global counts. Quarantine only with an owner, defect, and expiry date.

Interview Questions and Answers

Q: How would you test a paginated API beyond checking the first and last page?

I would define an expected ordered result set from deterministic fixtures, traverse every page, and compare the concatenated IDs with that oracle. I would assert page size, metadata, stable order, uniqueness across page boundaries, completeness, and behavior beyond the end. I would also test invalid parameters, authorization scope, concurrent inserts or deletes, and performance for deep traversal.

Q: What is the key QA difference between offset and cursor pagination?

Offset pagination is simple to navigate randomly, but inserts and deletes before the offset can shift records and create gaps or duplicates. Cursor pagination continues from a stable sort key and usually handles change better, but the token, filter binding, expiry, replay, and tampering behavior need coverage. Tests should match the consistency promise rather than applying identical expectations to both designs.

Q: How do you know a filter is complete, not merely correct?

Predicate checks prove that returned items match, but they do not prove all matches were returned. I create or query an independent expected set, traverse all result pages, and compare the complete ordered list. With less controlled data, I combine database checks, totals where reliable, and metamorphic relations such as a narrow result being a subset of a broader result.

Q: What should happen to the current page when a filter changes?

The behavior must be specified. A common and usable rule is to reset to page one because filtering changes result membership and may make the current page invalid. I verify the displayed page, URL, request, focus behavior, and browser history together so state cannot disagree across layers.

Q: How would you test stable sorting when values tie?

I seed multiple rows with the same primary sort value and unique IDs. I require a documented unique secondary key, retrieve the data repeatedly and across boundaries, then assert the exact composite-key order. I also test null handling if the sort field is nullable.

Q: How do you test pagination under concurrent data changes?

I fetch the first page, perform a controlled insert or delete at a known sort position, and continue traversal. I look for duplicated or skipped existing records and compare the result with the documented snapshot or continuation semantics. For cursor APIs, I verify the new record does not invalidate scope or allow a token to cross filter or tenant boundaries.

Q: Which tests belong in the browser and which belong at the API layer?

The API layer should hold most boundaries, invalid values, filter combinations, traversal, and authorization checks because it is faster and more diagnostic. Browser tests should cover parameter wiring, visible results, control states, URL and history behavior, accessibility, loading, empty and error views, plus race conditions that depend on client rendering.

Common Mistakes

  • Checking only that each page has no more than the requested limit. This misses wrong records, gaps, duplicates, and incorrect order.
  • Using an unstable sort. Any nonunique primary sort field needs a deterministic tie-breaker that the tests also assert.
  • Deduplicating records inside a test helper. That makes a defective API appear correct and destroys evidence.
  • Trusting total count as the sole completeness oracle. Counts may be stale, approximate, wrongly scoped, or correct while membership is wrong.
  • Testing filters one at a time only. Production failures often occur when filters, sorting, pagination, caching, and authorization interact.
  • Reusing mutable shared data while asserting exact counts. Parallel tests and background jobs then create false failures or conceal defects.
  • Adding browser sleeps for loading. Wait on the relevant response or an observable UI state and test stale-response protection deliberately.
  • Ignoring URL encoding and browser history. A view that works once may fail when refreshed, shared, or restored with Back.
  • Treating cursor contents as a client contract. Validate token behavior and security while keeping the cursor opaque.
  • Logging full query values and tokens. Observability must help diagnosis without exposing sensitive filter data.

Conclusion

Testing pagination and filtering is a proof about the whole accessible result set, not a spot check of page controls. Start with a precise contract and deterministic data, assert membership, uniqueness, order, metadata, predicates, and scope across boundaries, then exercise change, race, encoding, and error behavior.

Keep broad combinations in fast API tests and reserve browser automation for state integration and user experience. As a next step, choose one real endpoint, seed an exact boundary plus one record, and build a traversal assertion that would fail on either a gap or a duplicate. That single test often exposes assumptions worth fixing before the full matrix is added.

Interview Questions and Answers

How would you test a paginated API end to end?

I would seed deterministic records, establish the exact sorted oracle, and traverse all pages. I would assert schema, page metadata, size, filter truth, unique IDs, stable composite-key order, completeness, and end behavior. Then I would add invalid inputs, tenant isolation, concurrent mutation, cache separation, and performance coverage.

Why is a unique tie-breaker important in pagination?

A nonunique sort field leaves the relative order of tied records undefined. Across requests, tied rows can move between pages and create apparent gaps or duplicates. A unique secondary field such as ID makes traversal deterministic and testable.

What defects are common with offset pagination?

Common defects include off-by-one offsets, wrong last-page metadata, duplicates or gaps when data changes, unstable sorting, expensive deep offsets, and inconsistent handling of invalid limits. I target exact boundaries and mutations before the current offset because they expose these risks efficiently.

How do you validate filter correctness and completeness?

I first assert every returned item satisfies all active predicates. I then traverse the full result and compare it with an independently derived expected set, which proves completeness. I also cover invalid syntax, type boundaries, null semantics, encoding, and authorization scope.

How would you test a cursor for security problems?

I would alter, truncate, replay, and expire it, then combine it with changed filters, sorting, page size, and identity. The server should reject invalid context safely and never broaden access. Error messages and logs must not reveal cursor internals or sensitive query data.

How do you avoid flaky pagination tests?

I isolate test data by run, control timestamps and sort values, await observable responses instead of sleeping, and avoid asserting exact counts against shared mutable datasets. The traversal helper retains duplicates and fails on unexpected shapes so it cannot hide instability.

What should a pagination UI accessibility test cover?

It should cover accessible control names, keyboard operation, visible focus, disabled state, the current-page indicator, and announcements when results update. It should also verify zoom and narrow layouts, understandable empty states, and an accessible alternative for infinite scrolling.

Frequently Asked Questions

What are the most important pagination test cases?

Test empty data, one record, a partial page, an exact page, one record over the limit, an exact multiple, and a partial final page. Also verify invalid page inputs, stable order, duplicates across boundaries, behavior beyond the last page, and authorization scope.

How do I test pagination and filtering together?

Create a known ordered set for a filter that spans multiple pages, traverse it, and compare every returned ID with the expected pages. Then change, clear, and combine filters while checking page reset behavior, request parameters, URL state, totals, and visible results.

Should a filter change reset pagination?

Usually yes, because the new result set may not contain the current page. The product contract should decide, and QA should verify the page indicator, request, URL, browser history, and displayed records remain consistent.

How can QA detect duplicate records across pages?

Collect identifiers from every page without deduplicating them, then compare the size of the identifier set with the collected item count. Use deterministic fixtures and assert the exact sequence as well, because uniqueness alone does not detect missing records.

How is cursor pagination testing different from offset testing?

Cursor tests follow the opaque token and validate continuation, expiry, corruption, replay, filter binding, and tenant binding. They also test inserts and deletes between requests, while offset tests focus more heavily on shift-related gaps, duplicates, and deep-offset behavior.

What is a good oracle for filter testing?

For controlled integration tests, use a database query or independently computed expected list based on deterministic fixtures. Avoid importing the production filtering function into the test because the same defect could then exist in both implementation and oracle.

How many pagination cases should run in CI?

Run a compact risk-based set on each change, including empty, exact boundary, boundary plus one, compound filter, invalid input, scope isolation, and one browser flow. Larger pairwise, mutation, accessibility, deep-page, and performance suites can run on a schedule.

Related Guides