Делаем простые отчеты в CI с помощью GitLab Pages для Playwright

от автора

Привет, Хабр! Я Саша Алексеев, старший инженер по тестированию в Selectel. Некоторое время назад мы столкнулись с необходимостью реорганизовать отчеты end-to-end-тестов. Их прогон стал занимать слишком много времени, мы искали способы распределить их еще больше. Под катом я поделюсь вариантом, к которому мы в итоге пришли.

Сейчас у нас параллельное выполнение тестов в разных джобах с использованием shard=x/y, а общий репорт мы научились склеивать из отчетов разных прогонов. Хотел бы обсудить это решение в комментариях. Возможно, вы решаете подобную проблему иначе. Выбранный вариант описал в виде инструкции на тестовом стенде, так что сможете легко повторить его в собственных целях.

Введение

Долгое время для прогона тестов в GitLab CI нам хватало существующих публичных (shared) раннеров, которые нам предоставляли наши DevOps-специалисты. Для Playwright, с которым мы работаем, было доступно 5 vCPU и 8 ГБ RAM — для запуска 4-5 воркеров хватало.

Спустя время прогон тестов стал переваливать за 30 минут — мы начали искать способ параллелить тесты еще больше. И были альтернативные варианты, которые нам не подошли по нескольким причинам:

  • Можно было выделить отдельные раннеры с большим лимитом, но на момент решения проблемы ресурсов не было, их пришлось бы ждать. Было удобнее использовать общие раннеры.
  • Запускать меньшее количество тестов только по измененному функционалу было сложно. Playwright умеет запускать тесты по названию, но не по метаинформации. А у нас были очень длинные названия тестов, так мы их запускаем по компонентам (в одном тесте их может быть до 20).

Нашим решением стало параллельное выполнение тестов в разных джобах с использованием shard=x/y. В статье я поделюсь не только схемой отчетов e2e-тестов на Playwright при параллельном выполнении на разных нодах. Но и покажу упрощенную версию нашего CI, а также способ объединять HTML-отчет от Playwright-теста в один большой. Последнее необходимо, поскольку переключаться каждый раз по артефактам каждой джобы неудобно.

Итак, наш план:

  1. Инициируем Playwright-проект с GitLab CI-конфигурацией.
  2. Делаем отправку сообщений в Rocket.Chat после прогона тестов отдельным скриптом.
  3. Добавляем единный HTML-отчет для параллельных запусков.

Начало проекта с простым CI

Тесты

Для начала создадим проект в Playwright. У него есть команда init для примера:

npm init playwright@latest 

На выходе получим папки tests и tests-example. В конфигурации по умолчанию тесты запускаются только в /tests, так что переместим тесты из test-example в tests и проверим, сколько у нас теперь тестов:

npx playwright test --list … Total: 78 tests in 2 files 

Все работает — для примера этого достаточно. Теперь немного модифицируем конфигурацию наших тестов в playwright.config.ts. Если конкретно — добавим репорты list и junit, а также уберем проекты firefox и webkit.

Итоговая конфигурация

import { defineConfig, devices } from '@playwright/test';  export default defineConfig({   testDir: './tests',   /* Run tests in files in parallel */   fullyParallel: true,   /* Fail the build on CI if you accidentally left test.only in the source code. */   forbidOnly: !!process.env.CI,   /* Retry on CI only */   retries: process.env.CI ? 2 : 0,   /* Opt out of parallel tests on CI. */   workers: process.env.CI ? 3 : undefined,   /* Reporter to use. See https://playwright.dev/docs/test-reporters */   reporter: [['html'], ['list'], ['junit', { outputFile: 'test_results.xml' }]],   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */   …   projects: [     {       name: 'chromium',       use: { ...devices['Desktop Chrome'] },     },      // {     //   name: 'firefox',     //   use: { ...devices['Desktop Firefox'] },     // },      // {     //   name: 'webkit',     //   use: { ...devices['Desktop Safari'] },     // }, }); 

Мы убрали проекты firefox и webkit, так как они могут привести к путанице в junit-отчетах. В Playwright названия тестов в репорте не включают проект, для которого они были запущены. И при парсинге junit-отчетов 78 тестов в подробном отчете превращаются в 26. Причина в том, что названия некоторых тестов совпадают — к слову, на общее число тестов это не влияет.

Вот так это будет будет выглядеть.

CI

Итак, у есть набор тестов с разными репортами и тремя воркерами на поток в будущем CI. Настало время описать наш yml-файл для GitLab CI:

stages:   - tests e2e:   image: mcr.microsoft.com/playwright:latest   stage: tests   parallel: 5   allow_failure: true   tags:     - kube-dev   script:     - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL   artifacts:     paths:       - "**/test_results.xml"       - "**/playwright-report"     reports:       junit: "**/test_results.xml" 

Для CI используем GitLab, так как это основной инструмент непрерывной интеграции в Selectel. Перед созданием файла gitlab-ci.yml не забудьте активировать CI/CD в настройках проекта (хотя по умолчанию он должен быть включен).

Несколько комментариев по поводу конфигурации CI:

  • В поле image указана самая последняя версия образа, но советую «запинить» на текущую. Внезапные обновления Playwright могут что-нибудь сломать — от screenshot-тестов до ранее написанного кода.
  • В поле parallels указано число параллельных джоб. Число 5 выбрано для примера, в вашем случае оно может быть любым.
  • tags — тег для конфигурации. По нему GitLab определяет, на каких раннерах запускать тесты. Тег kube-dev предоставлен нам для разных проектов и управляется отделом DevOps-инженеров Selectel.

На этом шаге мы организовали параллельный запуск наших тестов с результатами в GitLab (из junit) и HTML-отчетами в артефактах каждой джобы.

Теперь настроим отправку сообщений в Rocket.Chat в нужном нам формате.

Сделаем отправку сообщений в чат с помощью вебхуков. Для примера будем использовать Rocket.Chat (в плане API он похож на Slack). Отдельный этап с репортом мы выбрали по двум причинам:

  1. Playwright делит тесты параметром —shard примерно на равные части, но по времени выполнения эти части могут сильно отличаться. А для репорта мы должны дождаться завершения всех параллельных запусков.
  2. Если запускать скрипт на отдельном этапе, после всех тестов можно неплохо «покостылить» и добавить отправку репортов в Jira, проверку количества автотестов карантине. Или склейку HTML-отчетов, отправку сообщений с ссылками на репорт, закрытие тестрана в TMS (при наличии интеграции) и другое. Конечно, если ничего этого не нужно, то достаточно настроить интеграцию или отправку сообщения с помощью вебхуков.

Всю настройку сообщение можно свести к плану:

  1. Добавляем вебхук от чата и токен на чтение проекта в переменные окружения проекта в GitLab CI.
  2. Пишем скрипт отправки сообщения.
  3. Добавляем новый stage в файл gitlab-ci.yml с запуском скрипта.

Успех!

Добавляем токен и вебхук

Токен добавляем как TOKEN, а вебхук от чата как ROCKET_WEBHOOK в переменную окружения. Переменную окружения стоит добавить в проект целиком по инструкции от GitLab.

Отправка сообщения в Rocket.Chat

Скрипт решил написать на TypeScript и библиотеке got, запускать все будет с помощью ts-node.

npm i got ts-node 

Просто так версии ts-node (10.9.1) и got (12.6.0) вместе не заработают, так что добавим в корень проект tscofnig.json и поменяем package.json.

1. Создаем файл tsconfig.json:

{     "compilerOptions": {         "module": "NodeNext", // or ES2015, ES2020         "moduleResolution": "no"     },     "ts-node": {         // Tell ts-node CLI to install the --loader automatically         "esm": true     }, } 

2. Добавляем параметр в package.json:

{ ….   "type": "module", … } 

3. Создадим файл со скриптом:

Скрипт reportToChat.ts

import got from 'got';  <h3> predefined variables gitlab</h3> const CI_API_V4_URL = process.env.CI_API_V4_URL const CI_PROJECT_ID = process.env.CI_PROJECT_ID; const CI_PIPELINE_URL = process.env.CI_PIPELINE_URL; const CI_PIPELINE_ID = process.env.CI_PIPELINE_ID;  <h3> specific variable in CI</h3> const ROCKET_WEBHOOK = process.env.ROCKET_WEBHOOK; const TOKEN_FOR_GET_GITLAB_REPORT = process.env.TOKEN; interface IGitlabResponse {   total_count: number;   failed_count: number;   skipped_count: number;   test_suites: object; } class Report implements IReport {   public total: number;   public failed: number;   public skipped: number;   constructor(response: IGitlabResponse) {     this.total = response.total_count;     this.failed = response.failed_count;     this.skipped = response.skipped_count;   } } interface IReport {   total: number;   failed: number;   skipped: number; } interface ISendRocketChat {   reportUrl: string,   report: IReport, } async function getGitlabReport(): Promise<IReport> {   const headers = { 'PRIVATE-TOKEN': TOKEN_FOR_GET_GITLAB_REPORT };   const url = `${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/test_report`;   try {     const responseJson: IGitlabResponse = await got.get(url, { headers: headers }).json();     return new Report(responseJson);   } catch (error) {     throw new Error('Не удалось загрузить репорт с gitlab\n' + error);   } }; async function sendRocketChatMessage(param: ISendRocketChat): Promise<void> {   const resultMsg = param.report.failed === 0 ? 'Успешно :white_check_mark:' : 'Найдены ошибки :sos:'   const payload = {     text:       `Ссылка на [JUnit отчёт](${param.reportUrl})\n` +       `${resultMsg}\n` +       `\`\`\`\nTotal:   ${param.report.total}\n` +       `Failed:  ${param.report.failed}\n` +       `Skipped: ${param.report.skipped}\n\`\`\``   };   await got.post(ROCKET_WEBHOOK as string, { json: payload }); } async function main() {   const test_report = await getGitlabReport();   try {     await sendRocketChatMessage({       reportUrl: `${CI_PIPELINE_URL}/test_report`,       report: test_report,     });   } catch (error) {     throw new Error('Не удалось отправить сообщение в Rocket.Chat\n' + error);   } } main(); 

Добавляем новый stage GitLab CI

Остается добавить запуск нашего скрипта с репортами в gitlab-ci.yml после выполнения всех тестов. Добавляем как отдельную джобу, что будет дожидаться завершения всех запущенных тестов:

  reports:    image: mcr.microsoft.com/playwright:latest    stage: report   needs: [e2e]   tags:     - kube-dev   before_script:     - npm i ts-node got   script:     - npx ts-node helpers/reportToChat.ts 

Теперь у нашего параллельного запуска есть репорты с количеством упавших и пройденных тестов. Но HTML-отчеты все еще у каждой части свои — давайте это исправим.

Единый HTML-отчет

Изначально склейка репортов была сделана с помощью библиотеки, playwright-merge-html-reports которую мы нашли благодаря issue #10437. Однако с версии Playwright 1.37.0 можно пользоваться встроенным и более удобным функционалом. В отличие от библиотеки тут решена проблема склейки произвольного числа репортов.

Все, что нам нужно сделать, можно разбить на несколько групп:

  1. обновляем playwright-test до 1.37.0 или выше. Добавляем blob-репорт
  2. добавляем в сообщения Rocket.Chat ссылку на итоговый репорт,
  3. дорабатываем наш CI для генерации общего отчета.

Сначала укажем новый репортер в playwright.config.ts вместо HTML и сделаем blob-репорт.

/* Reporter to use. See https://playwright.dev/docs/test-reporters */   reporter: [['blob'], ['list'], ['junit', { outputFile: 'test_results.xml' }]], 

Доработаем скрипт отправки сообщения

Нам нужно доработать сообщение в rocket-chat, добавив в него ссылку на HTML-отчет. В ссылке для каждого прогона будет меняться только JOB_ID. Итоговый вид нашего скрипта reportToChat.ts — ниже.

Скрипт отправки сообщения

import got from 'got';   // predefined variables gitlab const CI_API_V4_URL = process.env.CI_API_V4_URL const CI_PROJECT_ID = process.env.CI_PROJECT_ID; const CI_PIPELINE_URL = process.env.CI_PIPELINE_URL; const CI_PIPELINE_ID = process.env.CI_PIPELINE_ID; const JOB_ID = process.env.CI_JOB_ID   // JOB_ID для ссылки на html отчет const PAGES_URL = process.env.CI_PAGES_URL?.replace(/(?<=[a-z])\//, '/-/')// PAGES_URL также для ссылки на html отчет // specific variable in CI const ROCKET_WEBHOOK = process.env.ROCKET_WEBHOOK; const TOKEN_FOR_GET_GITLAB_REPORT = process.env.TOKEN;  interface IGitlabResponse {   total_count: number;   failed_count: number;   skipped_count: number;   test_suites: object; }  class Report implements IReport {   public total: number;   public failed: number;   public skipped: number;   constructor(response: IGitlabResponse) {     this.total = response.total_count;     this.failed = response.failed_count;     this.skipped = response.skipped_count;   } }  interface IReport {   total: number;   failed: number;   skipped: number; }  interface ISendRocketChat {   reportUrl: string,   htmlURL: string,// добавляем в интерфейс ссылку на html отчет   report: IReport, }  async function getGitlabReport(): Promise<IReport> {   const headers = { 'PRIVATE-TOKEN': TOKEN_FOR_GET_GITLAB_REPORT };   const url = `${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/test_report`;   try {     const responseJson: IGitlabResponse = await got.get(url, { headers: headers }).json();     return new Report(responseJson);   } catch (error) {     throw new Error('Не удалось загрузить репорт с gitlab\n' + error);   } };  async function sendRocketChatMessage(param: ISendRocketChat): Promise<void> {   const resultMsg = param.report.failed === 0 ? 'Успешно :white_check_mark:' : 'Найдены ошибки :sos:'    const payload = {     text:       `Ссылка на [JUnit отчёт](${param.reportUrl}) на [HTML](${param.htmlURL})\n` + // добавляем ссылку на наш отчет в сообщение       `${resultMsg}\n` +       `\`\`\`\nTotal:   ${param.report.total}\n` +       `Failed:  ${param.report.failed}\n` +       `Skipped: ${param.report.skipped}\n\`\`\``   };   await got.post(ROCKET_WEBHOOK as string, { json: payload }); }  async function main() {   const test_report = await getGitlabReport();   try {     await sendRocketChatMessage({       reportUrl: `${CI_PIPELINE_URL}/test_report`,       htmlURL: `${PAGES_URL}/-/jobs/${JOB_ID}/artifacts/playwright-report/index.html`, // собираем ссылку на HTML отчет       report: test_report,     });   } catch (error) {     throw new Error('Не удалось отправить сообщение в Rocket.Chat\n' + error);   } }  main(); 

Здесь все изменения рассчитаны на работу в CI. Так две переменные окружения (PAGES_URL и JOB_ID) нужны для составления URL и открытия HTML-репорта. Его также можно выгружать в стороннее хранилище и не хранить как артефакт сборки — тогда в htmlURL стоит указать адрес отчета во внешнем хранилище

Правим конфигурацию CI

В конфигурации CI мы должны учесть новый артефакт — папку blob-report, а также добавить склейку HTML-отчета из набора blob-отчетов. Итоговый вид нашего файла gitlab-ci.yml:

stages:   - tests   - report e2e:   image: mcr.microsoft.com/playwright:latest   stage: tests   parallel: 5   allow_failure: true   tags:     - kube-dev   before_script:     - npm i   script:     - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL   artifacts:     when: always      expire_in: 1 week     paths:       - "**/test_results.xml"       - "**/blob-report"  # это еще один вариант отчета в playwright     reports:       junit: "**/test_results.xml" reports:   image: mcr.microsoft.com/playwright:latest   stage: report   needs: [e2e]   tags:     - kube-dev   before_script:     - npm i ts-node got   script:     - npx playwright merge-reports --reporter=html ./blob-report # добавляем склейку отчета встроенной утилитой     - npx ts-node helpers/reportToChat.ts   artifacts:     when: always  # т.к. теперь в артефактах хранится отчет     expire_in: 1 week # оставляем его на 1 неделю     paths:       - "**/blob-report"       - "**/playwright-report"  

В рамках CI добавляется только склейка отчетов на последнем этапе. Это удобнее утилиты playwright-merge-html-reports. Для нее нужно было узнать кол-во параллельных выполнений тестов, передать это в stage "report" и настроить сохранение отчетов в разные папки для каждой параллели.

И вот, на последнем этапе у нас появляется артефакт "**/playwright-report", внутри которого будет созданный единый отчет. А в сообщения Rocket.Chat теперь содержится ссылка на единый отчет:

Заключение

Исходники проекта доступны по ссылке. Делайте форк и предлагайте свои улучшения!

Примерно этот код лежит в основе тестов одного из наших проектов. За исключением того, что у нас описано больше тестов. Также мы добавили дополнительные функции для удобства — например, отображение репорта в Jira Issue и интеграцию с TMS Qase для создания гибридных тест-ранов, где часть тестов проводится автоматически, а часть — руками.

В планах — внедрить подобную параллелизацию с единым отчетом для тестов сайта и панели управления. Также хотим попробовать выгружать HTML-репорты во внешнее хранилище с отображением списка отчетов, которое не привязано к GitLab CI, — некий аналог Allure Server, но для playwright-html.

Мы точно дойдем до запуска только необходимых е2е-тестов в зависимости от изменений — вероятно, это будущая тема для статьи. Основная сложность — запуск по метаинформации тестов при использовании @playwright-test. Сейчас она доступна только в названии теста. Следите за обновлениями в нашем блоге на Хабре!

Другие полезные материалы


ссылка на оригинал статьи https://habr.com/ru/articles/765012/


Комментарии

Добавить комментарий

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