Перейти до основного вмісту
Знижка 50% усі плани, обмежений час. Від $2.48/mo
13 min left
Інструменти розробника та DevOps

Як розгорнути власне візуальне регресійне тестування у своєму CI-конвеєрі

S Автор: Sajjad 13 хв читання
Self-hosted visual regression testing running in a CI pipeline with BackstopJS and Docker

На вашому ноутбуці візуальні регресійні тести проходять. Ви робите push, конвеєр запускається — і ті самі тести або мовчки кладуть браузер, або позначають п'ятдесят «змін», які насправді є лише різницею в згладжуванні. У коді нічого не змінилося, але CI має іншу думку.

Ця прірва між «працює локально» і «працює в CI» майже завжди є проблемою інфраструктури, а не тестів. Щоб надійно розгорнути власне візуальне регресійне тестування в CI-конвеєрі, потрібно правильно з'єднати чотири частини: CI-раннер з Docker-екзекутором, контейнер, налаштований так, щоб headless Chromium не залишався без спільної пам'яті, інструмент порівняння, який видає звіти, зрозумілі для CI, і VPS, підібраний під ту пам'ять, яку headless-браузери справді споживають.

Цей посібник збирає такий стек від початку до кінця. Наприкінці у вас буде робочий .gitlab-ci.yml (або еквівалент для Gitea), середовище Docker, у якому Chromium не падає, BackstopJS, що видає звіти JUnit, зрозумілі вашому конвеєру, і правильно підібраний VPS, щоб усе це запустити.

TL;DR

  • Запускайте браузер в одному й тому самому образі Docker локально та в CI. Більшість хибнопозитивних відмінностей виникає через різницю шрифтів і рендерингу між середовищами. Зафіксуйте образ — і вони зникнуть.
  • Виправте типове значення /dev/shm у 64MB. Розмір спільної пам’яті за замовчуванням у Docker не залишає ресурсів рендерер-процесу Chromium, і той мовчки падає в CI. Задайте shm_size: '2gb' у docker-compose.yml або передайте Chromium прапорець --disable-dev-shm-usage.
  • Використовуйте BackstopJS як основний інструмент для власного розгортання. Він має ліцензію MIT (v6.3.25), орієнтований на Docker завдяки прапорцю --docker і видає звіти JUnit. Якщо ви вже користуєтеся Playwright, його вбудований toHaveScreenshot() — цілком пристойний старт без жодних встановлень.
  • Беріть 4GB RAM за базовий орієнтир. Закладайте пам'ять із запасом. На практиці в CI задача зі знімками екрана в Chromium під час захоплення повної сторінки споживає значно більше пам'яті, ніж здається за станом простою, тож 4GB RAM — безпечніший орієнтир для однієї-двох послідовних або злегка паралельних задач.

Що цей посібник не охоплює

Це посібник зі складання інфраструктури, яку ви вже вирішили розгорнути. Кілька речей свідомо залишено поза межами:

  • Це не повне порівняння інструментів. Посібник зосереджений на стеку, який реально обслуговувати у власному CI-середовищі: BackstopJS, Docker і ваш власний раннер.
  • Він не охоплює власне розгортання Argos. Argos має відкритий код, але його публічний продуктовий сценарій і документація зосереджені на хмарному застосунку Argos та CI-інтеграціях, а не на простому шляху самостійного розгортання у продакшені. Для мети цього посібника BackstopJS — надійніший робочий приклад.
  • Він не рекомендує Lost Pixel. Цей проєкт заархівовано у квітні 2026 року; для нових розгортань використовуйте BackstopJS.
  • Він не переповідає суперечку «власний хостинг проти керованого SaaS». Якщо ви тут, то Percy чи Chromatic ви вже відкинули.

Передумови

Перед першою командою знадобиться кілька речей:

  • Власний CI-раннер або план його розгорнути (про це нижче).
  • Docker, встановлений на хості раннера.
  • Проєкт на Node.js із наявною ціллю для тестування (URL запущеного застосунку або набір маршрутів компонентів для захоплення).
  • Доступ до оболонки на VPS чи хості, де живе раннер.

Чому візуальні тести проходять локально, але ламаються в CI

Браузер, який рендерить ваш еталон на macOS, — це не той браузер, що рендерить порівняння в Linux-контейнері CI. Документація самого Playwright говорить про це прямо: рендеринг у браузері може відрізнятися залежно від OS хоста, версії, налаштувань, апаратного забезпечення, джерела живлення, headless-режиму та інших чинників. Рекомендація так само пряма: щоб знімки екрана були стабільними, запускайте тести в тому самому середовищі, де були згенеровані еталонні знімки екрана.

Саме ця рекомендація і є причиною всього подальшого налаштування. Майже за кожним хибним падінням стоїть одна з трьох невідповідностей:

  • Різниця у шрифтах і згладжуванні. На вашій робочій машині й у контейнері CI різні пакети шрифтів і різний субпіксельний рендеринг. Сторінки з великою кількістю тексту дають відмінності на кожному запуску, навіть коли нічого не змінювалося.
  • Headless-рендеринг проти рендерингу з інтерфейсом. Браузер з інтерфейсом і headless-браузер можуть трохи по-різному будувати макет однієї й тієї самої сторінки.
  • Вичерпання спільної пам'яті. Це тиха проблема. Типовий /dev/shm у Docker — 64MB, а headless Chromium використовує спільну пам'ять для своїх рендерер-процесів. Коли вона вичерпується, Chromium падає без жодної корисної помилки. Ваша задача просто вмирає або зависає.

Перші дві проблеми вирішує фіксація одного образу Docker і для генерації еталона, і для порівняння. Третю розв'язано в розділі про конфігурацію Docker нижче. Виправте всі три — і тести, що були «нестабільними в CI», стануть детермінованими.

Three reasons visual regression tests pass locally but fail in CI: font and anti-aliasing differences between the dev machine and the CI container, headless versus headed rendering differences, and shared-memory exhaustion from Docker's 64MB /dev/shm default crashing headless Chromium

Вибір інструмента: BackstopJS чи вбудоване порівняння Playwright

Якщо ви вже використовуєте Playwright для наскрізних тестів, найшвидший шлях — його вбудоване твердження toHaveScreenshot(): жодних додаткових залежностей, попіксельне порівняння та еталони з суфіксом платформи з коробки. Для невеликого набору тестів це цілком прийнятна зупинка, а командам, у яких приблизно менше п'ятдесяти екранів і немає спільної бібліотеки компонентів, більшого може ніколи й не знадобитися.

Дорога закінчується там, де починається процес рецензування. Порівняння в Playwright — суто попіксельне, без UI для перегляду в PR, тож затвердити правомірну партію візуальних змін означає перегенерувати знімки вручну. Коли кількість еталонів зростає, цей ручний цикл затвердження стає болючим.

BackstopJS — спеціалізований інструмент, створений саме для цього циклу. Він має ліцензію MIT (наразі v6.3.25), орієнтований на Docker через прапорець --docker і видає JUnit XML, який CI-раннер читає нативно. Його робота з еталонами (згенерувати референс, протестувати проти нього, затвердити прийняті відмінності як новий еталон) — саме та операційна модель, до якої зрештою приходить більшість власних VRT-конфігурацій. У решті посібника BackstopJS буде робочим прикладом.

Два інструменти, які вам можуть радити деінде, у цьому розборі не розглядаються: Argos — тому що його публічний продуктовий сценарій зосереджений на хмарному застосунку та CI-інтеграціях, а Lost Pixel заархівовано. Для практичного власного розгортання BackstopJS — надійніший робочий приклад.

ВісьBackstopJSPlaywright toHaveScreenshot()
Процес рецензування в PRВбудований звіт і команда approveРучна перегенерація знімків
Керування еталонамиКоманди reference / test / approveФайли знімків для кожного тесту
Рендеринг, нормалізований через Dockerпрапорець --dockerПринесіть свій образ
Витрати на встановленняОкрема залежністьУже є, якщо ви користуєтеся Playwright

Для глибшого зіставлення більшої кількості інструментів і критеріїв наше порівняння BackstopJS, Argos і Lost Pixel розглядає вибір повністю.

Розгортання власного CI-раннера на VPS

Стабільність рендерингу насправді забезпечує Docker-екзекутор: раннер виконує кожну задачу всередині контейнера, який визначаєте ви, тож середовище браузера щоразу однакове. Платформу раннера обирайте за тим, що ще ви хочете на неї покласти.

Власний GitLab CE — важчий варіант: репозиторії, CI/CD, реєстр, задачі та запити на злиття в одному екземплярі. Для цього посібника важливий Docker-екзекутор GitLab Runner, який дає змогу щоразу виконувати кожну задачу візуальної регресії всередині одного й того самого зафіксованого образу контейнера. GitLab, Gitea, Jenkins, Forgejo, Portainer і Docker доступні як розгортання в один клік у маркетплейс Cloudzy, що суттєво скорочує налаштування.

Gitea з act-runner — легка альтернатива. Gitea — це Git-сервіс на Go з рушієм робочих процесів, сумісним із GitHub Actions (через act-runner), тож якщо ваша команда звикла до синтаксису Actions, це компактний і швидкий у розгортанні раннер.

Що б ви не обрали, мета конфігурації однакова: раннер, який приймає задачі й виконує їх через Docker-екзекутор. Усе подальше (конфігурація контейнера, BackstopJS, зразок конвеєра) виходить із того, що цей екзекутор уже на місці.

Налаштування Docker, щоб Chromium не падав

Ось збій, що коштує людям найбільше часу. За замовчуванням Docker монтує /dev/shm розміром 64MB. Headless Chromium використовує цей розділ спільної пам'яті для своїх рендерер-процесів, а під час знімка повної сторінки йому потрібно значно більше за 64MB. Коли пам'ять вичерпується, рендерер помирає, часто без жодного повідомлення, що вказувало б на спільну пам'ять. Задача зависає, вилітає за тайм-аутом або повідомляє про загальний збій браузера.

Є два виправлення, і працює будь-яке з них.

Виправлення A: збільшіть розмір спільної пам'яті в Compose. Ключ shm_size задає розмір розділу /dev/shm усередині контейнера. довідник із файлу Docker Compose від Docker підтверджує, що shm_size налаштовує обсяг спільної пам'яті, дозволений контейнеру сервісу. Задайте його в блоці сервісу:

# docker-compose.yml
services:
  vrt:
    image: backstopjs/backstopjs:6.3.25
    shm_size: '2gb'          # override the 64MB default
    volumes:
      - ./:/src
    working_dir: /src

Виправлення B: скажіть Chromium взагалі не використовувати /dev/shm. Прапорець --disable-dev-shm-usage змушує Chromium писати файли спільної пам'яті в /tmp замість розділу спільної пам'яті. Передайте його в аргументах запуску браузера. У конфігурації backstop.json це engineOptions; у Playwright — launchOptions:

// backstop.json (fragment)
{
  "engine": "puppeteer",
  "engineOptions": {
    "args": ["--disable-dev-shm-usage", "--no-sandbox"]
  }
}

Виправлення A акуратніше, коли ви керуєте файлом Compose; виправлення B — переносний варіант, коли ви можете змінювати лише прапорці запуску браузера. Застосувати обидва не зашкодить.

Порада: Використовуйте абсолютно однаковий образ Docker локально та в CI. BackstopJS дає це задарма через прапорець --docker, який виконує захоплення всередині зафіксованого еталонного образу, тож ваша машина і раннер рендерять однаково. Саме це усуває хибнопозитивні спрацювання через шрифти та згладжування: зафіксуйте образ один раз і припиніть ганятися за примарними відмінностями.

Two fixes for headless Chromium crashing in Docker CI: raising the container's shared memory with shm_size set to 2gb in docker-compose.yml, or passing the --disable-dev-shm-usage flag so Chromium writes shared-memory files to /tmp instead of the 64MB /dev/shm partition

Налаштування BackstopJS для CI

BackstopJS створює звіт JUnit XML, за яким ваш CI-раннер вирішує, пройшла збірка чи ні, — у цьому і весь сенс вбудовувати його в конвеєр, а не запускати вручну. Встановіть його та ініціалізуйте конфігурацію:

npm install --save-dev backstopjs
npx backstop init

Далі вкажіть у backstop.json сторінки, які потрібно захоплювати, і ввімкніть звіт для CI:

// backstop.json (fragment)
{
  "id": "my_app_vrt",
  "viewports": [
    { "label": "desktop", "width": 1440, "height": 900 }
  ],
  "scenarios": [
    {
      "label": "Homepage",
      "url": "http://app:3000/",
      "selectors": ["document"]
    }
  ],
  "report": ["CI"],
  "engine": "puppeteer",
  "engineOptions": {
    "args": ["--disable-dev-shm-usage", "--no-sandbox"]
  }
}

Саме налаштування звіту для CI видає JUnit XML. Використовуйте прапорець --docker, коли запускаєте ці команди локально зі своєї хост-машини. Усередині CI-задачі, яка вже використовує образ backstopjs/backstopjs, запускайте backstop test напряму. Робота з еталонами зводиться до трьох команд:

npx backstop reference --docker   # capture the approved baseline
npx backstop test --docker        # run the comparison, emit the report
npx backstop approve              # accept the current diffs as the new baseline

Одне застереження щодо еталонів: кожна правомірна зміна UI означає запуск approve, щоб благословити нові знімки. Після приблизно сотні сценаріїв цей крок затвердження стає реальною операційною витратою: хтось має переглянути відмінності очима й вирішити, які з них навмисні, і це навантаження зростає разом із командою. Таке обслуговування — справжня ціна візуального регресійного тестування у великому масштабі, і її варто закласти в бюджет, перш ніж заводити великий набір тестів.

Робоча конфігурація CI-конвеєра

Конвеєр працює у дві логічні частини: один раз (або за потреби) згенерувати еталон, а потім порівнювати з ним на кожній зміні. Нижче наведено конфігурацію GitLab CI з Docker-екзекутором, у якій виправлення спільної пам'яті вже застосовано через прапорці запуску в backstop.json.

# .gitlab-ci.yml
stages:
  - visual-regression

visual_regression:
  stage: visual-regression
  image:
    name: backstopjs/backstopjs:6.3.25
    entrypoint: [""]
  script:
    - backstop test
  artifacts:
    when: always
    reports:
      junit: backstop_data/ci_report/xunit.xml
    paths:
      - backstop_data/bitmaps_test
      - backstop_data/html_report
    expire_in: 1 week

Рядок junit передає звіт до GitLab, щоб падіння з'являлися в інтерфейсі запиту на злиття; рядок paths зберігає растрові зображення відмінностей, щоб ви могли подивитися, що саме змінилося. У Gitea з act-runner ті самі кроки перекладаються на робочий процес у стилі Actions:

# .gitea/workflows/visual-regression.yaml
name: Visual Regression
on: [push, pull_request]

jobs:
  vrt:
    runs-on: ubuntu-latest
    container:
      image: backstopjs/backstopjs:6.3.25
      options: --shm-size=2gb          # raise /dev/shm for the job container
    steps:
      - uses: actions/checkout@v4
      - run: backstop test

Зверніть увагу на опцію контейнера --shm-size=2gb у робочому процесі Gitea. Це те саме виправлення спільної пам'яті, що й виправлення A, застосоване на рівні контейнера задачі, де немає окремого файлу Compose для редагування.

Підбір VPS під headless Chromium

Обмеженням тут є RAM, а не CPU. Навантаження на CPU в headless Chromium стрибкоподібне (пік під час захоплення і простій між ними), тож скромної кількості ядер цілком вистачає. Стелею, яка визначає, скільки задач можна виконувати одночасно, є пам'ять. Процес headless Chromium у простої тримається приблизно на 300–500MB і піднімається до 1–2GB під час знімка повної сторінки, тож ключовий чинник підбору — обсяг RAM на одну паралельну задачу.

RAMПідходить для
2GBЖорсткий мінімум: одна послідовна задача
4GBРекомендований орієнтир: 1–2 паралельні задачі
8GB3–4 паралельні задачі
16GBВеликі набори тестів і паралельні конвеєри

Ці цифри — практичні орієнтири з реальних запусків CI, а не вендорський бенчмарк, тож сприймайте їх як приблизну відправну точку і стежте за власними піками пам'яті.

Головне з розділу: 4GB RAM — надійний орієнтир; додавайте приблизно 2GB запасу на кожну додаткову паралельну задачу візуальної регресії.

Підступ самостійно розміщеного VRT у тому, що ранер, якому бракує пам’яті, не падає з гучною помилкою — він тихо кладе Chromium, а це саме той симптом, заради уникнення якого вся ця конструкція й існує. Закласти достатній запас пам’яті — найдешевша страховка, яку можна купити. Якщо збирати хост для ранера з нуля не хочеться, у маркетплейсі Cloudzy є розгортання в один клік для GitLab на власному сервері та Gitea на власному сервері , які за кілька хвилин дають робочий екземпляр CI на VPS, який ви можете підібрати рівно під той запас для headless Chromium, якого потребує ваш набір тестів.

VPS memory sizing guide for headless Chromium visual regression jobs: 2GB for a single sequential job, 4GB as the recommended baseline for one to two concurrent jobs, 8GB for three to four concurrent jobs, and 16GB for large suites and parallel pipelines

Часті запитання

Як налаштувати візуальне регресійне тестування у CI/CD-конвеєрі?

Встановіть BackstopJS, вкажіть у backstop.json цільові сторінки та ввімкніть опцію звіту для CI, щоб отримувати JUnit XML. Запускаючи з локальної машини, використовуйте backstop reference --docker і backstop test --docker, щоб знімки рендерилися всередині зафіксованого образу Docker. У CI виконуйте задачу всередині образу backstopjs/backstopjs і викликайте backstop test напряму, а потім публікуйте звіт JUnit із backstop_data/ci_report/xunit.xml. Використовуйте власний раннер із Docker-екзекутором, розрахований щонайменше на 4GB RAM для headless Chromium.

Скільки RAM потрібно headless Chromium у CI-контейнері?

Процес headless Chromium у простої споживає приблизно 300–500MB і піднімається до 1–2GB під час знімка повної сторінки. Для надійного CI розраховуйте приблизно на 4GB RAM на одну-дві паралельні задачі, додаючи близько 2GB на кожну наступну паралельну задачу. Навантаження на CPU стрибкоподібне і обмеженням не є; саме RAM визначає, скільки задач можна виконувати одночасно.

Чому мої візуальні регресійні тести проходять локально, але падають у CI?

Рендеринг у браузері відрізняється між середовищами: шрифти, згладжування, headless-режим проти режиму з інтерфейсом і сама OS змінюють результат. Виправлення — виконувати генерацію еталона й порівняння в одному образі Docker, щоб обидва рендерилися однаково. Прапорець --docker у BackstopJS робить це, виконуючи захоплення всередині зафіксованого еталонного образу.

Як виправити помилку /dev/shm у Chromium в Docker-CI?

Типовий /dev/shm у Docker має 64MB, що замало для рендерера headless Chromium і призводить до тихих падінь. Або збільште його через shm_size: '2gb' у docker-compose.yml (чи --shm-size=2gb на контейнері задачі), або передайте --disable-dev-shm-usage у прапорцях запуску Chromium, щоб він писав файли спільної пам'яті в /tmp.

Що обрати: Playwright toHaveScreenshot чи спеціалізований інструмент візуальної регресії?

Вбудованого toHaveScreenshot() у Playwright вистачає для невеликих наборів: приблизно до п'ятдесяти екранів, без спільної бібліотеки компонентів і без потреби в UI для перегляду в PR. Переходьте на спеціалізований інструмент на кшталт BackstopJS, коли потрібен повноцінний процес затвердження еталонів, звіт для рецензування та рендеринг, нормалізований через Docker, — це стає важливим, щойно кількість еталонів зростає.

Чи підтримується Lost Pixel досі?

Ні. Lost Pixel заархівовано у квітні 2026 року, і для нових розгортань він не підходить. Для власного візуального регресійного тестування використовуйте BackstopJS.

Поділитися

Більше з блогу

Продовжуйте читати.

Готові розгортати? Від $2,48/міс.

Незалежна хмара з 2008 року. AMD EPYC, NVMe, 40 Gbps. Повернення коштів за 14 днів.