Перейти к содержимому

Тестирование по эталонным файлам

Spec: ISO/IEC/IEEE 29119-4 Spec: ISO/IEC 25010 Evidence: Test-backed

Эталонный файл — это зафиксированный образец “так выглядит корректный результат”, с которым тест сравнивает вывод при каждом запуске. NextPDF использует эталоны, чтобы находить изменения, которые никто не собирался вносить: иначе сжатый поток, сместившийся абзац или отклонившуюся координату. На этой странице объясняется, как это работает и как эталон остаётся надёжным, а не превращается в устаревший образец, который никто не читает.

Генерация PDF — это длинный конвейер с множеством точек, где отклонение может пройти незаметно. Рефакторинг, который “ничего не меняет”, может незаметно переупорядочить операторы, изменить матрицу преобразования или сдвинуть ячейку таблицы на крошечную величину. Модульные тесты редко это улавливают: они проверяют значение, которое вы решили проверить, а не тысячи байт, оставшиеся без проверки. Методы на основе структуры и методы на основе спецификации находят разные ошибки, и ни один из них не покрывает другой (ISO/IEC/IEEE 29119-4, Annex A). Эталонный файл — это спецификация на примере: она фиксирует весь результат, а не одну проверку.

Риск есть в обе стороны. Слишком строгий эталон падает при каждом безобидном изменении, и его начинают вслепую утверждать заново, пока он не перестаёт что-либо доказывать. Слишком свободный эталон пропускает настоящие регрессии. Всё мастерство — в этом балансе.

  • Под эталонным файлом понимают зафиксированный эталонный результат, сгенерированный на основе заведомо корректного поведения движка и закоммиченный в репозиторий.
  • Эталонный тест заново генерирует результат и сравнивает его с зафиксированным эталоном; любое расхождение приводит к провалу теста и требует решения человека.
  • NextPDF сравнивает на уровне, который значим, но стабилен: извлечённый текст и нормализованные структурные операторы, а не сырые байты, потому что сырые байты несут шум (метки времени, порядок подмножеств, сжатие), который не является регрессией.
  • Обновление эталона — намеренное, проверенное действие через явный переключатель GOLDEN_UPDATE, а не автоматическое “принять всё, что изменилось”.
  • Эталонное тестирование отличается от снимков (snapshot) и характеризационного тестирования одним решающим образом: эталон никогда не обновляется автоматически.

Эталонная инфраструктура движка использует явное двухуровневое сравнение, а не побайтовую проверку:

  1. Generate Render the fixture input through the current engine.
  2. Layer 1 — text Extract human-readable text from the content stream; diff against the text golden. Catches dropped or reordered content and encoding regressions.
  3. Layer 2 — structure Extract ordered PDF operators, normalise coordinates to a fixed precision, diff against the operator golden. Catches layout shifts and broken structure.
  4. Decide Any diff fails the test; a human judges whether it is a regression or an intended change.
Как выполняется эталонное сравнение в NextPDF: сгенерировать PDF, извлечь значимые слои (текст, затем нормализованные структурные операторы), сравнить каждый из них с зафиксированным эталоном и при любом расхождении завершиться провалом с понятным человеку отчётом.

То, что инфраструктура намеренно не сравнивает, так же важно, как и то, что она сравнивает. Сырой байтовый вывод исключён: метки времени, порядок подмножеств шрифтов и сжатие потоков делают его хрупким, но не более корректным. Попиксельное сравнение изображений исключено из этого уровня, потому что требует внешнего средства отрисовки и добавляет вариативность окружения. Координаты с плавающей запятой нормализуются до фиксированной точности, чтобы бессмысленный шум округления не выглядел как регрессия. В этом разница между эталоном, который проверяет поведение, и эталоном, который проверяет преходящий шум окружения.

Этот выбор также задаёт профиль воспроизводимости страницы: структурный. NextPDF документирует три профиля — побитовый (воспроизводятся точные байты), структурный (воспроизводятся граф объектов и последовательность операторов, допускается безобидная вариативность на уровне байт) и семантический (воспроизводится смысл). Эталонные тесты на этом уровне по своей конструкции утверждают структурный профиль. Поэтому их эталоны переживают обновление библиотеки сжатия, но всё же падают при смещении таблицы.

Evidence: Test-backed Двухуровневое сравнение (извлечение текста, затем сравнение нормализованных структурных операторов) — это собственная документированная эталонная методология движка. В ней побайтовое сравнение, сравнение изображений и сравнение точных значений с плавающей запятой явно выведены за рамки по причинам, изложенным выше. Эталонный набор тестов — это объявленный, отдельно запускаемый набор, отличный от наборов снимков (snapshot) и характеризационных тестов.

Evidence: Test-backed Механизм честности конкретен: эталоны — это сгенерированные артефакты, а не написанные вручную. Их перезапись ограничена явным переключателем окружения GOLDEN_UPDATE, документированным как редкая, всегда проверяемая операция. Напротив, тесты снимков (snapshot) в движке регенерируются при первом запуске и подтверждают отклонение через флаг обновления. А характеризационные тесты фиксируют унаследованное поведение, не утверждая, что оно корректно. Это три намеренно разных инструмента.

Evidence: Standard-backed Эталон — это спецификация на примере. Spec: ISO/IEC/IEEE 29119-4, Annex A отмечает, что методы на основе спецификации и методы на основе структуры улавливают разные классы ошибок и что стратегия должна сочетать их. Именно поэтому эталоны стоят рядом с модульными и структурными тестами, а не вместо них, в пирамиде тестирования.

Эталонный тест механически прост; дисциплина — в рабочем процессе вокруг него:

<?php
declare(strict_types=1);
// 1. The fixture: a fixed HTML input committed next to the test.
// tests/Golden/fixtures/html-inputs/002-basic-table.html
// 2. The pinned references, generated once from known-good behaviour:
// 002-basic-table.text.golden (Layer 1 — extracted text)
// 002-basic-table.operators.golden (Layer 2 — normalised operators)
// 3. The run compares; ANY difference fails:
// vendor/bin/phpunit --testsuite Golden
// 4. An intended behaviour change is the ONLY time references move,
// and it is explicit and reviewed — never automatic:
// GOLDEN_UPDATE=1 vendor/bin/phpunit --testsuite Golden
//
// The regenerated *.golden files land in the diff of the same change
// that altered behaviour, so a reviewer sees the output delta next to
// the code delta and signs off on both together.

Пример и есть процесс. Код теста только выполняет сравнение. Эталону можно доверять потому, что изменившийся эталон проверяется как результат в том же изменении, которое затронуло движок.

Самая распространённая ошибка — считать эталонное тестирование побайтовым. Эталоны NextPDF — это не байты файла, а его извлечённый текст и нормализованные структурные операторы. Проверка сырых байт падала бы при новой версии zlib, другом теге подмножества или заново сгенерированной метке времени, хотя ничто из этого не является регрессией. Такой тест за неделю повторно утвердили бы до полной бесполезности. (Там, где точные байты действительно должны воспроизводиться, используется отдельный, более строгий побитовый профиль воспроизводимости, а не эталон.)

Вторая ошибка — полагать, что зелёный эталонный набор тестов доказывает корректность. Он доказывает отсутствие изменений. Эталон, сгенерированный из ошибочного результата, добросовестно защищает эту ошибку. Эталоны защищают от регрессии относительно заведомо корректной базовой точки; они не доказывают, что сама базовая точка была корректной. Для этого нужны уровни модульного, структурного тестирования и тестирования на соответствие.

Эталонный тест отвечает ровно на один вопрос: изменился ли результат относительно зафиксированного эталона. Он не говорит, был ли эталон когда-либо корректным. Он также не измеряет производительность, соответствие или безопасность. Это другие уровни. Размер набора фикстур, доля прохождения тестов и любой показатель покрытия — живые сигналы качества, генерируемые из артефактов непрерывной интеграции и публикуемые вместе со сборкой. Здесь они намеренно не приводятся, потому что могли бы устареть.

Точная структура каталогов, внутреннее устройство компаратора и переключатель обновления относятся к тестовой инфраструктуре движка и могут меняться. Если конфигурация тестов когда-либо расходится с этим объяснением, источником истины является она. Эта страница не делает никаких утверждений об инструментах снимков (snapshot) или эталонах других библиотек.

  • Эталонный файл — зафиксированный эталонный результат, сгенерированный на основе заведомо корректного поведения движка и закоммиченный; тест сравнивает с ним результат при каждом запуске. Никогда не обновляется автоматически.
  • Двухуровневое сравнение — эталонное сравнение NextPDF: извлечённый текст (Layer 1) и нормализованные структурные операторы (Layer 2) вместо сырых байт.
  • Тест снимка (snapshot) — родственный, но отдельный метод, при котором эталон регенерируется при первом запуске, а отклонение подтверждается через флаг обновления.
  • Характеризационный тест — тест, который фиксирует существующее поведение, не утверждая, что оно корректно, обычно чтобы сделать рефакторинг безопасным.
  • Профиль воспроизводимости — уровень, на котором результат должен воспроизводиться: побитовый (точные байты), структурный (граф объектов и последовательность операторов, допускается безобидная вариативность байт) или семантический (смысл). Эталонные тесты здесь утверждают структурный профиль.
  • GOLDEN_UPDATE — явный переключатель окружения, разрешающий перезапись эталонов; редкая, проверяемая операция.