Использование Pact, Puppeteer, Faker, Factory.ts и Unimocks

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

Инструменты, которые мы собираемся связать в единую структуру: инструмент пользовательского интерфейса (React), инструмент модульного тестирования (Jest), инструмент интеграционного тестирования (Puppeteer), инструмент тестирования контрактов (Pact) и инструмент для тестирования данных. генератор (Faker + Factory.ts). Все с помощью унифицированного инструмента для насмешек (Unimocks).

Концепции

Первым шагом к легкой разработке веб-приложения без API является определение строгого разделения задач между уровнями приложения, чтобы между ними не было посторонних зависимостей. Давайте посмотрим на следующую диаграмму, которая обозначает слои нашего приложения.

Вы можете найти подобные диаграммы в Интернете, особенно когда речь идет о разделении типов компонентов в React, с множеством определений этого различия, от умных/глупых компонентов до компонентов логики/представления. Терминология здесь неуместна, поскольку мы пытаемся следовать практике различения независимых компонентов, повторно используемых в приложении, от компонентов, привязанных к уровню данных.

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

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

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

Создание приложения

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

Это руководство предполагает определенный уровень опыта как в веб-разработке, так и в тестировании, поэтому, если вы обнаружите, что какие-то вещи вырываются из контекста запутанно или, наоборот, не хотите читать слишком упрощенные объяснения, вы можете найти исходный код настройка приложения здесь: https://github.com/mic-c/unimocks-demo

Прежде всего, давайте создадим приложение React, мы будем использовать CRA для ускорения процесса:

npx create-react-app my-app --template typescript

Простые компоненты

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

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

Универсальное создание данных

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

Для этого мы будем использовать комбинацию двух библиотек: factory.ts и Faker, чтобы создавать управляемые фабрики случайных данных. Давайте создадим его для наших пользователей.

Вы можете прочитать больше о Faker и factory.ts, но вот краткое описание того, что делает UserFactory. он предоставляет нам методы, которые генерируют один или несколько экземпляров пользователя с возможностью переопределения любых полей. Итак, если бы мы хотели создать старшего пользователя, мы могли бы вызвать

const user = UserFactory.build({ age: 70 });

каждый — это метод factory.ts, который позволяет нам генерировать нестатические данные. Он может быть привязан к индексу создаваемого объекта, но поскольку мы ленивы и нам нужны только случайные данные, мы делегируем это Faker.

Теперь, когда у нас есть способ легко генерировать поддельные данные, мы можем использовать его в нашем модульном тесте.

Имитация API

Наш следующий шаг — создать компонент, который будет отображать наш список пользователей, но для этого нам нужно откуда-то получить наш список пользователей, и, поскольку на данный момент у нас нет живого API, нам нужно создать фиктивный API. Как я уже упоминал ранее, мы хотим иметь возможность повторно использовать наши макеты как при тестировании, так и при разработке, поэтому для стандартизации нашего подхода к созданию макетов мы будем использовать Unimocks.

Сначала давайте определим наш API и формат ввода/вывода нашего запроса, которые мы упомянули на этапе концептуализации. Это будет метод, который выводит массив объектов User и не принимает входных параметров.

Теперь мы можем создать объект API как с живой, так и с фиктивной реализацией этих запросов. Как упоминалось ранее, мы полностью разделили живую и фиктивную реализации, помимо того, что связаны одним и тем же интерфейсом ввода-вывода, поэтому, если мы хотим, мы можем отложить живую реализацию на более поздний срок. Но в демонстрационных целях давайте воспользуемся axios для отправки нашего живого запроса API.

Как мы видим, мы определили нашу живую конечную точку, а также предоставили макет для создания 10 случайных пользователей для нас в разработке. Unimocks заменит любые вызовы этого метода предоставленными макетами, а также поможет нам позже с интеграционным тестированием, для чего будет использоваться имя «users».

Оптимизация сборки

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

Поскольку в этом примере используется Webpack, я написал небольшой загрузчик, который будет удалять код перед сборкой, поэтому давайте переместим наши моки в отдельный файл и пометим код для удаления, к счастью, оптимизация Webpack удалит неиспользуемый импорт.

Теперь осталось только добавить этот загрузчик в наш конфиг webpack. Простой, но далеко не идеальный пример того, как это сделать в CRA, см. в репозитории.

Компоненты, управляемые данными

Теперь, когда у нас настроен уровень запроса данных, давайте сделаем то же самое для нашего уровня управления данными. Это не тот уровень, который обязательно будет иметь каждое приложение, но если вы используете React, я настоятельно рекомендую использовать React Query, так как он очень прост в использовании и решает подавляющее большинство сценариев управления данными, которые вы, вероятно, будете запускать. в. Давайте создадим простой хук для получения нашего списка пользователей.

Теперь мы можем создать компонент списка пользователей, чтобы отображать то, что мы получаем от наших макетов.

Интеграционное тестирование

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

Настройка Puppeteer

Нам нужно, чтобы наш кукольник делал несколько вещей: запускал тесты с помощью `jest`, принимал машинописный текст и запускал сервер разработки для тестирования. Для начала установим все необходимые зависимости (их довольно много):

npm i --save-dev puppeteer @types/puppeteer @types/expect-puppeteer @types/jest-environment-puppeteer jest-dev-server jest-puppeteer

Теперь давайте перейдем к нашему tsconfig.json и добавим типы, которые мы только что установили:

"types": [
  "puppeteer",
  "jest-environment-puppeteer",
  "expect-puppeteer"
]

Далее давайте настроим все зависимости для совместной работы, начиная с нашей конфигурации интеграции jest. Обычно мы настраиваем jest с предустановкой «jest-puppeteer» для интеграции с puppeteer. С другой стороны, мы также обычно настраиваем jest с предустановкой ts-jest, чтобы писать наши тесты с помощью jest. Решение этого конфликта состоит в том, чтобы использовать только конфигурацию трансформатора из ts-jest.

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

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

Наконец, мы можем добавить пару скриптов в наш package.json для различных способов запуска наших тестов:

"test-integration": "jest --config ./puppeteer/jest.devserver.config.js",
"test-integration:serverless": "jest --config ./puppeteer/jest.config.js",
"test-integration:debug": "jest --config ./puppeteer/jest.config.js --debug",

Имитация интеграции

Теперь, когда мы действительно можем запускать интеграционные тесты, давайте посмотрим, как Unimocks может помочь нам с имитацией данных. Во-первых, давайте перейдем к нашему объекту API и добавим новую опцию.

{
  /*dev:start*/  mocks, /*dev:end*/,
  integrationMocks: !!process.env.REACT_APP_INTEGRATION
}

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

”start-integration”: “REACT_APP_INTEGRATION=true react-scripts start”

в наш package.json и не забудьте изменить команду в devserver.setup.js.

Теперь, когда «Unimocks» полностью настроен, мы можем перейти к рассматриваемому тесту, где мы добавляем следующее в наш «beforeAll»:

Это обеспечит перехват и имитацию всех запросов от usersAPI, а также позволит нам увидеть сделанные вызовы и проверить ввод запроса через userMocks.getUsers.calls. а также создавать собственные сценарии для тестирования, изменяя ответы с помощью userMocks.getUsers.setResponse и userMocks.getUsers.setError.

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

Объектные модели страниц

Если вы новичок в puppeteer и имеете опыт работы с Selenium или TestCafe, вы, вероятно, привыкли работать с вкладываемыми и настраиваемыми POM. К сожалению, puppeteer не предоставляет такой функциональности, и большинство людей просто предпочитают хранить селекторы в своих потенциальных полях POM, что неудобно и не может быть вложенным. К счастью, есть небольшая и простая утилита для создания правильных POM: pompeteer.

Тестирование контракта

Заключительная часть нашей настройки — тестирование контракта, которое позволит убедиться, что работающий API разработан правильно. Для контрактного тестирования мы будем использовать Pact и подмодуль unimocks/pact.

Настройка Pact

Первым шагом для интеграции наших контрактных тестов и генерации данных с unimocks является переопределение нашего API, добавление метаинформации, необходимой для контрактных тестов для каждого запроса, а также преобразование ее в функцию с базовым URL-адресом для связи с Pact.

Если вам не нравится писать дополнительный код, вы можете легко определить служебный метод для создания определений ваших запросов с помощью выбранной библиотеки http. Вот пример такого для axios.

Существуют также полезные функции filterFields и weedOutFields, с помощью которых можно легко разобрать, какие части входных данных нужно поместить в тело, параметры или заголовки запрос.

updateUser: axiosRequest({ baseURL, method: 'PATCH', path: ({id}) => `/users/${id}`, body: weedOutFields(['id'])}),

Теперь, когда мы добавили метаданные в наши запросы, мы можем использовать их для простого создания взаимодействий с pact через `ServiceInteractionBuilders`. Но прежде чем мы начнем, давайте взглянем на некоторые из лучших практик из документации Pact.

Если вы используете Pact Broker для обмена договорами, избегайте использования случайных данных в ваших договорах. Если публикуется новый договор, точно такой же, как и предыдущая версия, которая уже была проверена, существующие результаты проверки будут применены к новой публикации договора. Это означает, что вам не нужно ждать, пока завершится проверка поставщика перед развертыванием вашего потребителя — вы можете сразу перейти к производству. Случайные данные создают впечатление, что контракт изменился, и поэтому вы теряете эту оптимизацию.

Хотя это может показаться препятствием для нашей установки, на самом деле с помощью jest довольно просто управлять, настраивая макеты наших генераторов данных.

Давайте создадим установочный файл Jest для тестирования нашего соглашения и воспользуемся поддельной UserFactory, которая будет давать предсказуемые и согласованные данные.

И, наконец, давайте добавим соответствующий скрипт для запуска контрактных тестов:

"test-contract": "jest --roots \"<rootDir>/pact/tests\" --setupFilesAfterEnv \"<rootDir>/pact/factory-mocks/setup.ts\" --detectOpenHandles --runInBand --testMatch=\"**/?(*.)+(pacttest).[tj]s?(x)\"",

Использование ServiceInteractionBuilders

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

Так как мы хотим генерировать согласованные данные и запускать каждый запрос изолированно, мы вызываем resetSequenceNumber на каждой из наших фабрик после построения каждого взаимодействия. Теперь давайте добавим простой тест для конечной точки GET наших пользователей.

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

Заключение

Заключение

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

Некоторые советы, о которых следует помнить

  • Вы можете быстро отключить макеты для любой службы, находящейся в разработке, закомментировав параметр mocks.
  • В зависимости от того, насколько тщательным вы хотите, чтобы было покрытие вашего тестирования, может быть хорошей идеей добавить дополнительный слой небольших кросс-браузерных дымовых тестов с живыми API.
  • Чтобы избежать циклической зависимости и иметь более аккуратную файловую структуру, я бы посоветовал иметь каталог /services с подкаталогом, названным в честь каждой службы, т. е. /users, который будет содержать все интерфейсы и определения API в файле users.d.ts или users.types.ts, ваш объект API в файле users.api.ts, ваши макеты в файле users.mocks.ts, ваши фабрики в файле users.factories.ts и, наконец, ваше управление данными, которое является файлом большинство компонентов будут импортированы в файл users.services.ts.
  • При запуске интеграционных тестов убедитесь, что входные данные ваших запросов верны. Точно так же при первом добавлении макетов разработчика может помочь вывод входных данных, которые не используются в макете, чтобы убедиться, что вы ничего не пропустили.
  • Более гибкие и живые макеты CRUD можно найти в файле users.mocks.ts в репозитории. Этот подход обеспечивает гораздо большую гибкость для правильного тестирования и демонстрации вашего приложения, чем статические макеты.
  • Всегда проверяйте сгенерированный контракт, чтобы убедиться, что ваши совпадения закончились так, как вы планировали.

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