Pagination, Filtering, Sorting

Какие ошибки чаще всего скрываются в списочных API и как проверять пагинацию, фильтры и сортировку без поверхностного покрытия.

Обновлено:

Содержание

Списочные API кажутся простыми, но именно в них живёт много неприятных дефектов. Пока команда смотрит только на "список открывается", в фоне могут прятаться дубли, пропуски, нестабильный порядок, неверные total count, сломанные фильтры и несогласованность между страницами.

Для QA это важная тема, потому что списки - это почти везде: пользователи, заказы, платежи, логи, события, товары, задачи, комментарии. Если pagination, filtering и sorting реализованы плохо, продукт начинает вести себя "странно", а пользователи описывают это как хаос: записи пропадают, появляются дважды, фильтр работает не так, сортировка скачет, а на второй странице уже другие данные.

Зачем это нужно API

Когда данных мало, сервер может вернуть всё сразу. Но как только объём становится заметным, это перестаёт быть хорошей идеей: ответы становятся тяжёлыми, клиенту неудобно их рендерить, а backend тратит лишние ресурсы.

  • →pagination - возвращает данные частями
  • →filtering - отдаёт только нужный поднабор
  • →sorting - определяет порядок элементов

На практике эти три механизма почти всегда работают вместе. Пользователь открывает список заказов, фильтрует по статусу, сортирует по дате и листает страницы. Если хотя бы один слой работает нестабильно, ломается весь опыт работы со списком.

Pagination

Pagination - это разбиение большого набора данных на страницы или порции.

  • →offset-based pagination: page=2, limit=20 или offset=20&limit=20
  • →cursor-based pagination: cursor=..., starting_after=..., next_page_token=...

Для QA важно понимать разницу.

Offset pagination проще для чтения и часто встречается в админках и обычных CRUD API. Но у неё есть слабое место: если данные между запросами меняются, состав страниц может "плыть". На первой странице пользователь увидел 20 записей, потом кто-то добавил новую запись, и на второй странице уже появились дубли или пропуски.

Cursor pagination обычно устойчивее на живых данных. Она лучше подходит для больших, часто меняющихся списков, событийных лент, транзакций и high-load систем. Но её труднее тестировать глазами, потому что вместо понятного номера страницы приходится следить за курсором и порядком элементов.

Для QA отсюда главный вывод: pagination нужно проверять не только на статичном наборе, но и на изменяющихся данных.

Filtering

Фильтрация ограничивает набор данных по условиям.

  • →статус = active
  • →created_at >= дата
  • →price between X and Y
  • →owner_id = текущий пользователь
  • →search по имени или email
  • →несколько фильтров одновременно

На вид это очень удобная функция, но именно здесь часто ломается логика.

  • →фильтр частично игнорируется
  • →два фильтра вместе работают иначе, чем по отдельности
  • →пустой фильтр и отсутствующий фильтр трактуются по-разному
  • →чувствительность к регистру неожиданная
  • →фильтрация по дате ломается на timezone
  • →фильтр на backend работает не так, как UI ожидает
  • →часть данных скрыта из-за прав, и пользователь думает, что фильтр "сломался"

Для QA важно понимать, что filtering - это не только проверка правильного SQL-условия или query param. Это проверка того, что система возвращает именно тот срез данных, который обещает контракт.

Sorting

Sorting определяет порядок элементов в выдаче. Чаще всего это сортировка по дате, имени, статусу, приоритету, цене или другому полю.

Самая частая ошибка здесь в том, что сортировку проверяют слишком поверхностно: "вроде похоже на правильный порядок". На практике сортировка должна быть:

  • →корректной
  • →устойчивой
  • →предсказуемой
  • →совместимой с pagination

Особенно важна стабильность порядка. Если сортировка идёт только по полю с повторяющимися значениями, например по created_at с одинаковым timestamp или по status, порядок между одинаковыми элементами может плавать. А если поверх этого ещё включена пагинация, пользователь начинает видеть дубли и пропуски между страницами.

Для QA это значит, что сортировка должна проверяться не только "по одному полю", но и на tie cases, когда значения совпадают.

Что важно QA

Есть несколько вещей, которые почти всегда стоит проверять.

Для pagination:

  • →первая страница
  • →последняя страница
  • →пустая страница
  • →page size = 1
  • →page size на верхней границе
  • →page size больше допустимого
  • →переход между соседними страницами
  • →отсутствие дублей и пропусков между страницами
  • →корректность has_more, next_cursor, total_count, если они есть

Для filtering:

  • →один фильтр
  • →несколько фильтров вместе
  • →граничные значения
  • →пустое значение
  • →отсутствие фильтра
  • →невалидное значение фильтра
  • →фильтр по полям с правами доступа
  • →фильтр по дате и времени

Для sorting:

  • →ascending и descending
  • →сортировка по разным полям
  • →одинаковые значения у нескольких записей
  • →сортировка вместе с фильтрами
  • →сортировка вместе с pagination
  • →поведение при недопустимом поле сортировки

Где ломается чаще всего

  • →элемент есть и на первой, и на второй странице
  • →элемент пропадает между страницами
  • →total_count не совпадает с реальным количеством
  • →фильтр применяется к данным, но не к count
  • →сортировка визуально меняется между одинаковыми запросами
  • →новая запись внезапно "вклинивается" и ломает навигацию по offset pagination
  • →backend молча игнорирует неизвестный фильтр или сортировку
  • →UI думает, что сортировка одна, а API отдаёт другую
  • →фильтр по дате даёт разные результаты из-за timezone
  • →фильтр и права доступа вместе дают неожиданный результат

Это особенно важно в продуктах, где список - основной рабочий экран. Там даже мелкая нестабильность быстро превращается в пользовательское недоверие.

Практический пример

Представь API списка заказов:

  • →GET /orders?status=paid&sort=created_at_desc&limit=20

Что здесь важно QA:

  • →действительно ли пришли только paid заказы
  • →действительно ли они отсортированы по дате в нужную сторону
  • →если у двух заказов одинаковое created_at, порядок остаётся стабильным
  • →на следующей странице нет дублей и пропусков
  • →total_count, если он есть, соответствует отфильтрованному набору
  • →если во время листания появился новый заказ, pagination ведёт себя предсказуемо
  • →невалидный sort или status не ломает сервис и обрабатывается внятно

Один и тот же список может выглядеть "почти рабочим", но на практике быть ненадёжным именно из-за этих деталей.

Что должен вынести QA из этой темы

  • →Pagination, filtering и sorting нужно тестировать вместе, а не по отдельности.
  • →Списочные API часто ломаются не в happy path, а на границах и комбинациях условий.
  • →Offset pagination проста, но чувствительна к изменениям данных между запросами.
  • →Cursor pagination устойчивее на живых данных, но требует другой логики проверки.
  • →Стабильная сортировка критична для корректной pagination.
  • →total_count, has_more, курсоры и границы страниц нужно проверять так же внимательно, как и сами данные.

Что ещё почитать

  • →Внутри базы: Negative API testing
  • →Внутри базы: Методы, статус-коды, заголовки
  • →Внешний материал: Stripe pagination

Набор из 12 записей с известными страницами

Локальная фикстура начинает с ID 1–12. Score равен floor((id−1)/3), поэтому каждые три соседние записи имеют одинаковое значение. Чётные ID имеют status open, нечётные — closed. Сортировка score использует ID вторым ключом, чтобы порядок при равенстве был определён. Проверки ниже выполняются на чистой фикстуре без записей между запросами страниц.

plain text
Запрос	Ожидаемые ID	Total
?sort=score&limit=5&offset=0	1,2,3,4,5	12
?sort=score&limit=5&offset=5	6,7,8,9,10	12
?sort=score&limit=5&offset=10	11,12	12
?sort=score&limit=5&offset=12	(пусто)	12
?status=open&sort=score&limit=5&offset=0	2,4,6,8,10	6
?status=open&sort=score&limit=5&offset=5	12	6

Объедините первые три страницы и потребуйте ровно ID 1–12 без повторов и пропусков. Total должен описывать отфильтрованный набор до пагинации, а не длину текущей страницы. По этому контракту limit — от 1 до 5, offset — неотрицательное целое, неподдерживаемые сортировка и фильтр дают 400.

Определённый порядок при равенстве не создаёт snapshot. При изменении данных между offset-запросами записи могут переместиться между страницами. Отдельный эксперимент должен зафиксировать контракт согласованности и вставку/удаление между запросами; cursor-пагинации тоже нужны порядок и правила валидности курсора. Подготовленная фикстура проверяет статический offset, а не эти дополнительные гарантии.

Продолжить изучение

Скачать локальную учебную лабораторию (ZIP)

Проверка порядка и фильтров в SQL

Невалидные query-параметры