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

Из этой статьи вы узнаете, как захватывать фрагменты веб-страниц с помощью Cypress, а затем интегрируете этот инструмент тестирования в CI, чтобы в будущем никто не вносил в проект нежелательных изменений.

На создание этой стратегии тестирования меня мотивировала работа. В Thinkific есть внутренняя система проектирования, и мы добавили к ней Cypress, чтобы избежать сюрпризов при работе с файлами CSS/JS.

К концу этой статьи у нас будет репозиторий с тестами Cypress:

Прежде чем начать

Для имитации библиотеки компонентов (Component Library) я создал сэмпл веб-сайта. Это очень простой сайт, сделанный с помощью TailwindCSS и размещенный в Vercel. Он документирует два компонента: значок и кнопку.

С исходным кодом можете ознакомиться на GitHub. Веб-сайт статический и находится в папке public. Вы можете просмотреть веб-сайт локально, запустив npm run serve и перейдя в браузере на http://localhost:8000:

Добавление Cypress и Cypress Image Snapshot

Начните с клонирования репозитория-образца. Затем создайте новую ветку и установите Cypress Image Snapshot, пакет, отвечающий за захват и сравнение скриншотов.

git checkout -b add-cypressnpm install -D cypress cypress-image-snapshot

После добавления пакетов необходимо выполнить несколько дополнительных шагов, чтобы добавить в Cypress Cypress Image Snapshot.

Создайте файл cypress/plugins/index.js со следующим содержимым:

const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');

module.exports = (on, config) => {
  addMatchImageSnapshotPlugin(on, config);
};

Далее создайте файл cypress/support/index.js, содержащий:

import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';

addMatchImageSnapshotCommand();

Создание теста скриншотов

Пришло время создать тест для скриншотов. План таков:

  1. Cypress заходит на каждую страницу проекта (значок и кнопку).
  2. Cypress делает скриншот каждого примера на странице. Страница значка содержит два примера (по умолчанию и “таблетка”), в то время как страница кнопки содержит три (по умолчанию, “таблетка” и контур). Все эти примеры находятся внутри элемента <div> и cypress-wrapper. Этот класс был добавлен с единственной целью  —  определить, что нужно протестировать.

Первый шаг  —  создание конфигурационного файла Cypress (cypress.json):

{
  "baseUrl": "http://localhost:8000/",
  "video": false
}

BaseUrl здесь  —  веб-сайт, который запущен локально. Как уже было сказано, npm run serve поставляет содержимое папки public. Вторая опция, video, отключает для Cypress запись видео, поскольку в данном проекте нам это не понадобится.

Время создавать тест. В cypress/integration/screenshot.spec.js добавим:

const routes = ['badge.html', 'button.html'];

describe('Component screenshot', () => {
  routes.forEach((route) => {
    const componentName = route.replace('.html', '');
    const testName = `${componentName} should match previous screenshot`;

    it(testName, () => {
      cy.visit(route);
  
      cy.get('.cypress-wrapper').each((element, index) => {
        const name = `${componentName}-${index}`;
  
        cy.wrap(element).matchImageSnapshot(name);
      });
    });
  });
})

В коде выше динамически создаются тесты, основанные на массиве routes. Тест создаст по одному изображению на каждый элемент .cypress-wrapper, который есть на странице.

Наконец, внутри package.json создадим команду для запуска тестов:

{
  "test": "cypress"
}

И здесь появляются два варианта: запускать Cypress в headless-режиме через npm run cypress run или воспользоваться Cypress Test Runner через npm run cypress open

Вариант Headless

Вывод npm run cypress run должен быть похож на следующее изображение:

Тесты будут пройдены, и будут созданы пять изображений в папке /snapshots/screenshot.spec.js.

Вариант Test Runner

Через npm run cypress open откроется Cypress Test Runner, и вы сможете следить за тестами шаг за шагом.

Первую веху мы преодолели, так что давайте объединять текущую ветку с веткой master. Если хотите увидеть текущее состояние проекта, переходите прямо к моему пулл-реквесту.

Cypress внутри Docker

Если вы запустите тест выше, чередуя Headless и Test Runner, то, возможно, заметите, что скриншоты отличаются.

На компьютерах с retina-дисплеем, вы можете получить retina-изображения (2x) через Test Runner, в то время как headless-режим не предоставляет скриншотов в высоком качестве.

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

В Linux и Windows, например, полоса прокрутки у приложений видима, а macOS скрывает полосу прокрутки.

Если содержимое на скриншоте не соответствует компоненту, у вас может быть (или отсутствовать) полоса прокрутки. Если ваш проект опирается на шрифты, включенные в ОС по умолчанию, скриншоты также будут отличаться в зависимости от среды.

Чтобы избежать этих несоответствий, тесты будут выполняться внутри Docker, чтобы компьютер разработчика не влиял на снимки экрана.

Давайте начнем с создания новой ветки:

git checkout -b add-docker

Cypress предлагает различные Docker-образы  —  с подробностями можно ознакомиться в их документации и блоге.

Для этого примера я выбрал образ cypress/included, который включает в себя Electron и готов к использованию.

Нам нужно внести два изменения: поменять baseUrl в файле cypress.json:

{
  "baseUrl": "http://host.docker.internal:8000/",
}

и команду test в файле package.json:

{
  "test": "docker run -it -e CYPRESS_updateSnapshots=$CYPRESS_updateSnapshots --ipc=host -v $PWD:/e2e -w /e2e cypress/included:4.11.0"
}

При запуске npm run test возникнет проблема:

Изображения немного отличаются, но почему? Давайте посмотрим, что находится внутри папки __diff_output__ :

Именно то, о чем я упоминал чуть раньше: несоответствия шрифтов! Компонент Button (кнопка) использует шрифт, установленный в ОС по умолчанию. Поскольку Docker работает внутри Linux, отрисованный шрифт не будет таким же, как тот, который стоит на macOS.

Так как мы перешли на Docker, эти скриншоты устарели. Время их обновить:

CYPRESS_updateSnapshots=true npm run test

Пожалуйста, обратите внимание: я снабжаю тест-команду префиксом переменной окружения CYPRESS_updateSnapshots.

Вторая веха позади. На случай, если вам понадобится помощь, сравните ваш результат с моим пулл-реквестом.

Давайте сольем эту ветку с основной и двинемся вперед.

Добавление CI

Наш следующий шаг  —  добавление тестов в CI. На рынке существуют различные CI-решения, но для этого руководства я обращусь к Semaphore.

Конфигурация проста, и ее можно адаптировать к другим решениям, таким как CircleCI или Github Actions.

Прежде чем создавать конфигурационный файл Semaphore, давайте подготовим наш проект к запуску в CI.

Первый шаг  —  установка start-server-and-test. Как следует из названия пакета, он запустит сервер, дождется URL-адреса и затем выполнит тестовую команду:

npm install -D start-server-and-test

Во-вторых, отредактируйте файл package.json:

{
  "test": "docker run -it -e CYPRESS_baseUrl=$CYPRESS_baseUrl -e CYPRESS_updateSnapshots=$CYPRESS_updateSnapshots --ipc=host -v $PWD:/e2e -w /e2e cypress/included:4.11.0",
  "test:ci": "start-server-and-test serve http://localhost:8000 test"
}

В скрипте test мы добавляем переменную окружения CYPRESS_baseUrl. Это позволит динамически изменять базовый URL-адрес, используемый Cypress. Кроме того, добавим скрипт test:ci, который запустит только что установленный пакет.

И, наконец, Semaphore. Создайте файл .semaphore/semaphore.yml со следующим содержимым:

 1 version: v1.0
 2 name: Cypress example
 3 agent:
 4   machine:
 5     type: e1-standard-2
 6     os_image: ubuntu1804
 7 blocks:
 8   - name: Build Dependencies
 9     dependencies: []
10     task:
11       jobs:
12         - name: NPM
13           commands:
14             - sem-version node 12
15             - checkout
16             - npm install
17   - name: Tests
18     dependencies: ['Build Dependencies']
19     task:
20       prologue:
21         commands:
22           - sem-version node 12
23           - checkout
24       jobs:
25         - name: Cypress
26           commands:
27             - export CYPRESS_baseUrl="http://$(ip route | grep -E '(default|docker0)' | grep -Eo '([0-9]+\.){3}[0-9]+' | tail -1):8000"
28             - npm run test:ci

Давайте разберемся в этом подробнее:

  • Строки 1–6 определяют, какой тип экземпляра мы будем использовать в среде.
  • Строки 8 и 16 создают два блока: первый блок, Build Dependencies, будет запускать npm install, загружая нужные нам зависимости. Второй блок, Test, будет запускать Cypress с небольшими отличиями.
  • В строке 27 мы динамически задаем переменную окружения CYPRESS_baseUrl на основе IP-адреса, который в данный момент использует Docker. Это заменит http://host.docker.internal:8000/, который может работать не во всех средах.
  • В строке 28 мы наконец запускаем тест с помощью start-server-and-test: как только сервер будет готов к подключению, Cypress запустит тестовый набор.

Еще одна веха пройдена, пора делать слияние нашей ветки! По желанию просмотрите пулл-реквест, содержащий все файлы из этого раздела, и проверьте сборку внутри Semaphore.

Запись тестов в cypress.io

Чтение выходных данных тестов в CI не очень дружелюбно по отношению к пользователю. На этом этапе мы интегрируем наш проект с cypress.io.

Следующие шаги основаны на документации Cypress.

Давайте начнем с того, что получим идентификатор проекта и ключ записи. В терминале создайте новую ветку и запустите:

git checkout -b add-cypress-recording
CYPRESS_baseUrl=http://localhost:8000 ./node_modules/.bin/cypress open

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

Внутри Cypress перейдем на вкладку Runs, нажмем кнопку Set up project to record (“Настроить проект для записи”) и выберем имя и настройку видимости. Мы получим projectId, который автоматически добавляется в файл cypress.json, и закрытый ключ записи. Вот видео этих шагов:

В Semaphore я добавил ключ записи в качестве переменной окружения CYPRESS_recordKey. Теперь давайте обновим тестовый скрипт с учетом этой переменной:

{
  "test:ci": "start-server-and-test 'serve' 8000 'npm run test -- run --record --key $CYPRESS_recordKey'"
}

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

Пришло время сделать последнее слияние и закончить на этом интеграцию.

Тестирование в реальной жизни

Представьте, что мы работаем над изменением, которое влияет на заполнение кнопок: время проверить, будет ли Cypress улавливать разницу.

На примере веб-сайта давайте удвоим горизонтальное заполнение с 16px до 32px. Это довольно простое изменение, так как мы используем Tailwind CSS: px-4 заменяется на px-8. Вот пулл-реквест.

Как и следовало ожидать, Cypress зафиксировал, что кнопка не соответствует скриншотам. Посетив страницу, мы можем проверить скриншот сломанного теста:

Файл diff показывает исходный скриншот слева, текущий результат справа, и их наложение  —  в середине. Есть также возможность загрузить изображение, чтобы лучше рассмотреть проблему:

Если это на самом деле не проблема, обновите скриншоты:

CYPRESS_updateSnapshots=true npm run test

Конец

На сегодня все. Надеюсь, вы узнали, как с пользой применять Cypress, чтобы гарантировать, что никто не добавляет в проект неожиданные изменения.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Leonardo Faria: How to Add Screenshot Testing with Cypress to Your Project

Предыдущая статьяОбновления в Android 11: Scoped Storage и другие улучшения конфиденциальности
Следующая статьяЧто нужно знать, чтобы начать заниматься квантовыми вычислениями