Эта статья также опубликована на моем сайте: https://fwouts.com/articles/react-screenshot-test-journey

Это отрывок из внутренней документации, которую я написал для React Screenshot Test, библиотеки, которую я недавно создал, чтобы упростить написание тестов снимков экрана с помощью React.

Чтобы понять, как возникла внутренняя архитектура React Screenshot Test, давайте немного перемотаем назад.

Первоначальная идея была проста: что, если бы мы могли написать тесты для компонентов React, которые выглядели бы почти как тесты моментальных снимков, но сравнивали бы реальные скриншоты вместо HTML?

Если вы еще не знакомы со снимками состояния Jest, вот пример, взятый из их документации.

import React from "react";
import Link from "../Link.react";
import renderer from "react-test-renderer";
it("renders correctly", () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

Это создает следующий снимок визуализированного HTML:

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

Вместо этого я хотел создать снимок экрана:

Оказывается, создать снимок экрана из компонента React не так просто, как я надеялся.

Первое, что вам нужно, чтобы сделать снимок экрана фрагмента HTML, - это, конечно же, веб-браузер. К счастью, Google Chrome можно легко контролировать с узла с помощью библиотеки Puppeteer. Отлично, у нас есть браузер.

А что насчет HTML? Если вы знакомы с рендерингом на стороне сервера, возможно, вы уже знаете ответ: используйте ReactDomServer.renderToString()! Действительно, это именно то, что я использовал.

Я решил развернуть локальный сервер (названный «сервером компонентов»), который будет использовать рендеринг на стороне сервера для обслуживания HTML. Каждому «узлу» (компоненту React с определенным набором свойств) будет назначен случайный идентификатор и, следовательно, уникальный URL-адрес. Например, /render/abc-123 может показывать нашу замечательную ссылку на Facebook выше.

С этим локальным сервером сделать снимок экрана было несложно:

  • Добавьте узел к компонентному серверу и сохраните его сгенерированный идентификатор.
  • Откройте браузер с помощью Puppeteer.
  • Перейдите к http://localhost:[port]/render/[node-id].
  • Сделайте снимок экрана с page.screenshot().

Последней частью головоломки было сравнение снимков PNG. К счастью, сотрудники American Express создали снимок изображения-шутки, который делает именно это. Не нужно изобретать велосипед.

Все сделано!

Не совсем так. К моему большому разочарованию, как только я настроил React Screenshot Test на CircleCI, тесты не прошли. Была небольшая (2%) визуальная разница между снимками экрана, созданными на моем MacBook Pro, и снимками, созданными CircleCI.

Оказывается, рендеринг на разных платформах будет несовместимым. Ну облом. Но в этой ветке была интересная идея: что, если бы мы использовали Docker?

Один из вариантов - сказать: «Всегда запускайте тесты в Docker, иначе у вас будут плохие времена ™». Однако это было бы не лучшим опытом для разработчиков. Что, если бы React Screenshot Test без проблем запустил браузер в Docker за вас?

Это немного усложнило ситуацию. Если браузер работает в Docker, но тесты выполняются на главном компьютере, вы больше не можете просто использовать API Puppeteer для управления браузером. Они эффективно работают на разных машинах.

Как лучше всего общаться между разными машинами? HTTP, конечно!

Это привело к новой абстракции: «сервер скриншотов». Это HTTP-сервер с единственной конечной точкой:

POST /render { url: string } -> image/png

Реализовать это было несложно с помощью сервера Express, на котором запущен Puppeteer. Я создал образ Docker, который обернул все это в красивый пакет.

Затем я обновил логику скриншотов, чтобы общаться с сервером скриншотов вместо прямого использования Puppeteer.

Это сработало?

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

Зачем вообще запускать Docker внутри Docker? Это был ненужный уровень вложенности. Резюмируя:

  • На машине разработчика мы хотим запустить сервер скриншотов в Docker.
  • В Docker мы хотим запустить Puppeteer напрямую.

Еще одно ограничение связано с тем, как работает Jest.

Чтобы запускать тесты параллельно, Jest запускает несколько процессов Node. Поскольку это отдельные процессы, они не могут совместно использовать память. В частности, они не могут предоставить общий доступ к экземпляру Puppeteer. Это не идеально для совместного использования ресурсов: запуск нового двоичного файла Chrome для каждого тестового файла не очень хорошо масштабируется!

А что, если бы мы запустили один сервер скриншотов до наших тестов? Поскольку это HTTP-сервер, все процессы Node могут общаться с ним, если им известен его порт.

Решение, которое сначала может показаться немного запутанным, заключалось в следующем:

  • Запустите сервер скриншотов (локально или в Docker) с помощью глобального установочного крючка Jest.
  • Запускайте другой компонентный сервер в каждом тесте (экспресс-серверы дешевы).
  • Попросите сервер снимков экрана сделать снимки экрана с URL-адресов, обслуживаемых сервером компонентов.

Так появился React Screenshot Test.

Хотите узнать больше или даже внести свой вклад? Ознакомьтесь с остальной документацией в репозитории response-screenshot-test!