Q16 of 37 · API testing

How do you test pagination in an API?

API testingMidapipaginationcursorsedge-cases

Short answer

Short answer: Verify page boundaries (first, last, beyond-last), totals match, no records duplicated or skipped across pages, and pagination tokens or cursors are stable. Test with both small and large data sets — many bugs only show up at page 100+.

Detail

Pagination bugs are sneaky: small test datasets pass, prod fails on page 47.

Pagination styles to know:

  • Offset / limit: ?offset=100&limit=20. Easy to implement, expensive on large tables (DB has to skip 100 rows). Susceptible to drift if data changes between calls.
  • Page / per-page: ?page=5&per_page=20. Same as offset/limit with different naming.
  • Cursor-based: ?cursor=abc123&limit=20. The cursor encodes "next position." Stable under inserts/deletes, harder to "jump to page N."
  • Keyset: ?after=2026-05-10T12:00:00Z. Sort key as cursor.

What to test:

1. Page contents are stable and don't overlap:

const page1 = await request.get('/items?page=1&per_page=10').then(r => r.json());
const page2 = await request.get('/items?page=2&per_page=10').then(r => r.json());

const ids1 = page1.items.map(i => i.id);
const ids2 = page2.items.map(i => i.id);

expect(new Set([...ids1, ...ids2]).size).toBe(20);  // no duplicates

2. The total record count matches the sum of paginated results:

const all = [];
let page = 1;
while (true) {
  const res = await request.get(`/items?page=${page}&per_page=10`).then(r => r.json());
  all.push(...res.items);
  if (res.items.length < 10) break;
  page++;
}
expect(all.length).toBe(res.total);

3. Beyond-last page returns empty list, not error:

const res = await request.get('/items?page=99999&per_page=10');
expect(res.status()).toBe(200);
expect((await res.json()).items).toEqual([]);

4. Per-page boundaries: per_page=1, per_page=max, per_page=0 (likely 400), per_page=−1.

5. Stability under change (cursor-based): insert a new record between page calls; the cursor should still produce consistent results without duplicating or skipping.

6. Total field is correct: many APIs return total or total_pages. Compare against the actual count from the paginated walk.

7. Sorting consistency: pagination is meaningless without a stable sort. Test with explicit sort parameters; assert the same record orders across pages.

Common bugs:

  • Last item of page N appears as first of page N+1 (off-by-one).
  • "total" stops being correct after a delete.
  • Cursor returns the same page repeatedly.
  • per_page=0 hangs the server.

For very large datasets, write a test that walks 100+ pages and asserts no duplicates. Catches "race-with-inserts" bugs that smaller tests miss.

// EXAMPLE

// Walk every page and assert no duplicates / no skips
test('pagination covers all items without duplicates', async ({ request }) => {
  const seen = new Set<string>();
  let page = 1;
  let total = 0;

  while (true) {
    const res = await request.get(`/items?page=${page}&per_page=50`);
    const body = await res.json();
    total = body.total;

    for (const item of body.items) {
      expect(seen.has(item.id)).toBe(false);   // no duplicates
      seen.add(item.id);
    }

    if (body.items.length < 50) break;
    page++;
  }

  expect(seen.size).toBe(total);  // all items returned exactly once
});

// WHAT INTERVIEWERS LOOK FOR

All three pagination styles, knowing per-page boundaries to test, asserting on no-overlap and total correctness, and bonus awareness that cursor-based is more stable than offset.

// COMMON PITFALL

Testing only page 1 and 2 with 10 records total. Real bugs hide at page 100+ and require walking the full set to find duplicates or skips.