Интеграция Storybook с Cypress и HMR

В этом посте присоединяйтесь ко мне, поскольку я интегрирую 2 супер-инструмента Frontend — Storybook и Cypress — для создания реальных e2e тестов автоматизации, которые работают на основе историй Storybook.

Как мне кажется, высококачественный компонент должен иметь святую троицу — хорошие истории Storybook, хорошо охватывающие модульные тесты и хорошие e2e тесты автоматизации для него.

У нас, разработчиков FE, есть много инструментов, которые помогают нам достичь этой цели, но, похоже, между ними неизбежно возникает дублирование. Например, я тестирую обработку кликов моего React-компонента в Jest с помощью React Testing Library, а затем тестирую ту же функциональность с помощью Cypress (или любого другого e2e-фреймворка, который вы можете использовать).

Такое дублирование — это нормально. Каждый тип тестирования имеет свои преимущества. Тем не менее, мне стало интересно, можно ли повторно использовать части экосистемы FE dev и сократить объем обслуживания и кода, необходимого для запуска тестов автоматизации моих компонентов.

К концу этой заметки вы увидите, что это вполне возможно — я запущу тест Cypress над историей Storybook компонента, и все это будет поддерживать HMR (Hot Module Replacement), так что любое изменение в связанных файлах запустит тест снова.

Приступим к делу —

Когда я начал работать с этой идеей, первым вариантом, который пришел мне в голову, было запустить Storybook, а затем сказать Cypress перейти к исходному url iFrame компонента и начать взаимодействовать с ним.
Это может работать, но имеет некоторые сложности, например, убедиться, что Storybook запущен первым, и как к нему обращаться в окружениях, создаваемых по требованию в конвейере сборки, но затем мне представился другой метод — использование библиотеки, разработанной командой Storybook, под названием @storybook/testing-react.

Основная цель этой библиотеки — позволить разработчикам использовать уже написанную конфигурацию рендеринга компонента, выполненную в Storybook, для юнит-тестирования, но знаете что? Вы также можете использовать ее для рендеринга вашего компонента для тестов Cypress.

Я беру простой компонент Pagination из своего пакета @pedalboard/components, чтобы провести над ним несколько тестов. В настоящее время у него есть история Storybook, которая выглядит следующим образом:

import React from 'react';
import Pagination from '.';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
 title: 'Components/Pagination',
 component: Pagination,
 // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
 argTypes: {
   onChange:{ action: 'Page changed' },
 },
};

// // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <div><Pagination {...args} /></div>;

export const Simple = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Simple.args = {
   totalPages:10,
   initialCursor:3,
   pagesBuffer:5,
};
Вход в полноэкранный режим Выход из полноэкранного режима

А вот как это выглядит под Storybook:

Я знаю — проще не бывает 😉
Зададим требования к моим тестам следующим образом:

  1. Монтируем компонент, курсор которого установлен на «3» (как определено в сторибуке)
  2. Нажмите кнопку «PREV» 3 раза
  3. Утверждение, что кнопка «PREV» отключена и больше не может быть нажата.

Да, вы правы — это также можно проверить с помощью библиотеки тестирования react, но помните, что некоторые вещи нельзя, и более того, мы используем здесь реальный DOM.

Начнем с установки Cypress:

yarn add -D cypress
Войти в полноэкранный режим Выход из полноэкранного режима

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

yarn run cypress open
Войти в полноэкранный режим Выход из полноэкранного режима

Да, похоже, все работает хорошо. Cypress запускает браузер Chrome, и у меня есть куча примеров тестов в каталоге packages/components/cypress/integration, но в данный момент меня это не волнует.

Создание нашего тестового файла

Мне нравится хранить все тесты компонента в его собственной директории. Это относится и к тесту Cypress, который я собираюсь создать. Я буду придерживаться соглашения *.spec.js и создам файл под названием index.spec.js в каталоге компонента.

Текущее содержимое этого теста будет вставлено из документации Cypress:

describe('My First Test', () => {
  it('Does not do much!', () => {
    expect(true).to.equal(false)
  })
})
Вход в полноэкранный режим Выход из полноэкранного режима

Но при повторном запуске Cypress не находит вновь созданные тесты, и я не виню его, поскольку он не ищет в нужном месте. Давайте изменим это — в файл cypress.json я добавлю следующую конфигурацию:

{
   "testFiles": "**/*.spec.{js,ts,jsx,tsx}",
   "integrationFolder": "src"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Запускаю Cypress снова, и, конечно, мой тест проваливается, как и ожидалось. Мы на верном пути!

А теперь самое интересное…

Интеграция

Сначала мне нужно установить 2 ключевые библиотеки:

Первая — это @storybook/testing-react, о которой я упоминал в начале, которая позволит мне компоновать компонент из Story, или, другими словами, позволит мне «сгенерировать» готовый к рендерингу компонент из истории Storybook.

Второй — @cypress/react, который позволит мне смонтировать компонент, чтобы Cypress мог начать взаимодействовать с ним:

yarn add -D @storybook/testing-react @cypress/react
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь все становится немного сложнее —
Сначала я начну с дополнительных библиотек, которые мы должны установить и объяснить позже:

yarn add -D @cypress/webpack-dev-server webpack-dev-server
Вход в полноэкранный режим Выход из полноэкранного режима

Я настрою тестирование компонентов cypress на поиск тестов в директории src в файле cypress.json:

{
   "component": {
       "componentFolder": "src",
       "testFiles": "**/*spec.{js,jsx,ts,tsx}"
   }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Поскольку мы тестируем компоненты, я использую здесь ключ «component», чтобы определить, как он должен действовать. Подробнее об этом можно прочитать здесь.

Мы еще не закончили. Для поддержки HMR в тестах нам нужно настроить cypress на работу с плагином dev-server, который мы установили ранее. Для этого в файл cypress/plugins/index.js нужно добавить следующее:

module.exports = async (on, config) => {
   if (config.testingType === 'component') {
       const {startDevServer} = require('@cypress/webpack-dev-server');

       // Your project's Webpack configuration
       const webpackConfig = require('../../webpack.config.js');

       on('dev-server:start', (options) => startDevServer({options, webpackConfig}));
   }
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Если у вас острый глаз, вы, вероятно, заметили там ссылку на файл webpack.config.js. Да, это обязательное условие. Есть несколько способов сделать это (как описано здесь), но я решил использовать способ с пользовательским Webpack config.

Проект, для которого я это делаю, не имеет собственной конфигурации Webpack (пока), и моей первой попыткой было получить конфигурацию, которую использует Storybook, но это создало некоторые проблемы. Думаю, было бы неплохо иметь некий адаптер для получения и использования Webpack-конфигурации Storybook, подобно существующему адаптеру для получения Webpack-конфигурации Create-React-App.

Мой webpack.config.js для этой цели — это необходимый минимум. В нем нет ни точки входа, ни выхода. Только правила для babel-loader, style-loader и css-loader:

module.exports = {
   module: {
       rules: [
           {
               test: /.(jsx|js)$/,
               exclude: /(node_modules)/,
               use: {
                   loader: 'babel-loader',
                   options: {
                       presets: ['@babel/preset-env', '@babel/preset-react'],
                   },
               },
           },
           {
               test: /.css$/i,
               exclude: /(node_modules)/,
               use: ['style-loader', 'css-loader'],
           },
       ],
   },
};
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, когда я все это настроил, я могу модифицировать свой тест, чтобы начать взаимодействовать со Storybook. В настоящее время мой тест просто монтирует компонент Pagination и все. Никаких взаимодействий или утверждений пока нет:

import React from 'react';
import {composeStories} from '@storybook/testing-react';
import {mount} from '@cypress/react';
import * as stories from './index.stories.jsx';

// compile the "Simple" story with the library
const {Simple} = composeStories(stories);

describe('Pagination component', () => {
   it('should render', () => {
       // and mount the story using @cypress/react library
       mount(<Simple />);
   });
});
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте запустим тесты cypress и будем надеяться на лучшее 🙂 Я делаю это с помощью команды open-ct cypress, которая запустит только тестирование компонента.

yarn cypress open-ct
Вход в полноэкранный режим Выход из полноэкранного режима

Опаньки! Компонент отрисовывается в открытом браузере Cypress. Самое интересное, что вам не нужны новые инструкции рендеринга для тестируемого экземпляра компонента, вы используете инструкции рендеринга из истории 🙂

Наконец-то тестирование

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

Вот мой тест:

import React from 'react';
import {composeStories} from '@storybook/testing-react';
import {mount} from '@cypress/react';
import * as stories from './index.stories.jsx';

// compile the "Simple" story with the library
const {Simple} = composeStories(stories);

describe('Pagination component', () => {
   describe('PREV button', () => {
       it('should be disabled when reaching the first page', () => {
           // and mount the story using @cypress/react library
           mount(<Simple />);

           const prevButton = cy.get('button').contains('PREV');

           prevButton.click();
           prevButton.click();
           prevButton.click();

           prevButton.should('be.disabled');
       });
   });
});
Войти в полноэкранный режим Выход из полноэкранного режима

И да — сохранение этого файла запускает тест снова (HMR — это счастье), и он делает именно то, что я от него ожидал (и довольно быстро, добавлю я):

Вот и все, готово!

Подведение итогов

Итак, давайте посмотрим, что мы имеем —
У нас есть Cypress, выполняющий один тест на компоненте, конфигурация рендеринга которого импортирована из истории Storybook компонента. Каждый раз, когда я изменяю тесты, историю или компонент, Cypress запускает тест снова, что дает мне отличную немедленную обратную связь с любыми изменениями, которые я делаю.
Хотя интеграция не самая гладкая, какой она может быть, конечный результат того стоит.
Если у вас есть несколько историй для вашего компонента, вы можете смонтировать их и заставить Cypress выполнять различные тесты соответственно. Возможность повторного использования историй компонента для тестов Cypress значительно сокращает дублирование в конфигурации рендеринга и помогает в обслуживании тестов.

Довольно неплохо ;), но, как всегда, если у вас есть идеи, как сделать это лучше или другие техники, обязательно поделитесь с остальными!

Если вам понравилось то, что вы только что прочитали, заходите на @mattibarzeev в Twitter 🍻.

Фото Vardan Papikyan on Unsplash

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *