Автотесты в CI

Непрерывная интеграция (continuous integration, CI) - практика разработки, при которой участники команды вливают свои изменения в общую основную ветку не реже раза в день, а каждую интеграцию проверяет автоматическая сборка с тестами. Автотесты - та часть сборки, которая решает, можно ли принять изменение. CI-система запускает их по событиям в репозитории кода и по расписанию, собирает результаты и сообщает их обратно в изменение, команде и другим системам.

Непрерывную интеграцию как практику разработал Кент Бек в рамках экстремального программирования (Extreme Programming) в 1990-х годах. Мартин Фаулер опубликовал статью «Continuous Integration» в 2000 году и переработал её в январе 2024 года; в ней перечислены практики, среди них самотестирующаяся сборка, сборка при каждой отправке изменений в основную ветку и быстрая сборка. Сам термин иногда приписывают Гради Бучу, который, по словам Фаулера, лишь упомянул его мимоходом в книге об объектно-ориентированном проектировании. Программа ISTQB Advanced Level Test Automation Engineering (версия 2.0, 2024 год) посвящает отдельную главу встраиванию автотестов в пайплайны CI/CD: какие уровни тестирования выполняются на каком этапе и какие из них служат шлюзами качества.

Какую задачу решает CI

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

CI сокращает расстояние между изменением и его проверкой. Каждая отправка изменений или запрос на слияние запускает сборку и тесты, и автор получает результат, пока изменение ещё свежее. Требование к скорости Фаулер формулирует через правило экстремального программирования о десятиминутной сборке. Упавшую сборку исправляют сразу, чтобы основная ветка оставалась в рабочем состоянии.

Уровни автотестов (модульные, интеграционные, сквозные) и их стоимость описаны в статье Уровни автоматизации и пирамида тестов. Здесь описано, как эти тесты запускаются в CI и как используются их результаты.

События запуска пайплайна

Пайплайн (pipeline) - последовательность заданий, которые CI-система выполняет для одного события: сборка, запуск тестов, публикация артефактов, развёртывание. Пайплайн запускается по событию (trigger). Обычные события и тесты, которые по ним выполняются:

Событие Когда срабатывает Типичный набор тестов
Отправка изменений (push) В ветку отправлен коммит Быстрые тесты: модульные тесты, статические проверки
Запрос на слияние (pull или merge request) Запрос открыт или обновлён Тесты, нужные для принятия изменения
Расписание В заданное время, например каждую ночь Полная регрессия, долгие и нефункциональные тесты
Ручной запуск Человек запускает пайплайн Выбранный набор в выбранном окружении

В GitHub Actions события объявляются в ключе on рабочего процесса (workflow):

yaml
name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:
    inputs:
      suite:
        description: 'Test suite to run'
        required: true
        default: 'smoke'
        type: choice
        options:
        - smoke
        - regression

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v7
    - uses: actions/setup-node@v7
      with:
        node-version: lts/*
    - run: npm ci
    - run: npm test

Документация GitHub Actions задаёт подробности каждого события. По умолчанию pull_request срабатывает на типы действий opened, synchronize и reopened, а рабочий процесс выполняется на коммите слияния запроса (refs/pull/<number>/merge), поэтому тесты проверяют результат слияния, а не только ветку. schedule использует синтаксис cron POSIX, по умолчанию работает в UTC на последнем коммите ветки по умолчанию, не может запускаться чаще раза в пять минут и может задерживаться при высокой нагрузке. В публичных репозиториях запуски по расписанию отключаются после 60 дней без активности. workflow_dispatch работает, только если файл рабочего процесса лежит в ветке по умолчанию; его входные параметры могут иметь типы boolean, choice, number, environment и string.

Этапы по скорости

Тесты в пайплайне упорядочены от быстрых к медленным. Быстрый этап падает рано и не тратит ресурсы медленных этапов на изменение, которое уже сломано. Фаулер описывает это как конвейер развёртывания (deployment pipeline): сборка коммита (commit build) выполняется при каждой отправке изменений и должна быть быстрой, а вторичные сборки запускают более медленные тесты, например с настоящей базой данных, не блокируя каждый коммит.

Программа CTAL-TAE распределяет уровни тестирования по этапам:

  • компонентные (модульные) тесты выполняются на этапе сборки и служат шлюзами качества пайплайна; тесты интеграции компонентов выполняются вместе с ними;
  • тесты системной интеграции часто служат шлюзами качества пайплайна непрерывной поставки;
  • системные тесты могут выполняться в пайплайне непрерывного развёртывания как последний шлюз качества перед поставкой продукта;
  • системные и приёмочные тесты выполняются либо на этапе развёртывания, где падение может откатить развёртывание, либо в отдельном пайплайне, который запускается успешным развёртыванием.

Типичный порядок с условными длительностями:

push / pull request
 |
 +-- build, static checks               1-2 min
 +-- unit tests                         2-5 min    quality gate
 +-- integration tests                  5-10 min   quality gate
 +-- deploy to test environment
 +-- end-to-end tests, 4 shards         10-15 min  quality gate
 +-- report: JUnit XML, artifacts
nightly schedule
 +-- full regression, performance tests

В GitHub Actions порядок заданий задаётся через needs: задание начинается только после успешного завершения заданий, от которых оно зависит, а если одно из них упало или пропущено, зависимые задания пропускаются.

Параллельность и шардирование

Параллельность внутри одной машины и распределение между машинами - два разных уровня. Playwright запускает файлы тестов в параллельных рабочих процессах (workers); по умолчанию их число - половина логических ядер процессора. Его руководство по CI рекомендует один рабочий процесс на агентах CI ради стабильности и воспроизводимости, а для более широкого распараллеливания - шардирование.

Шардирование (sharding) - разбиение набора тестов на части, которые выполняются как независимые задания на разных машинах. В Playwright опция --shard=x/y выбирает часть x из y. С fullyParallel: true части балансируются по отдельным тестам; без неё целые файлы распределяются по шардам, и файлы разного размера дают шарды разной длительности. В GitHub Actions шарды создаются матрицей:

yaml
jobs:
  e2e:
    needs: unit
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
    - uses: actions/checkout@v7
    - uses: actions/setup-node@v7
      with:
        node-version: lts/*
    - run: npm ci
    - run: npx playwright install --with-deps
    - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

Матрица порождает не больше 256 заданий на один запуск рабочего процесса. Опция fail-fast по умолчанию равна true: когда одно задание матрицы падает, остальные отменяются. Для шардов тестов её ставят в false, чтобы каждый шард доработал и сообщил обо всех падениях. В GitLab CI то же делается ключевым словом parallel и переменными CI_NODE_INDEX и CI_NODE_TOTAL.

Каждый шард создаёт свой отчёт. Playwright объединяет их командой npx playwright merge-reports из blob-отчётов; для JUnit XML каждый шард выгружает свой файл, а объединяет их CI-система или принимающая система.

Отчёты о тестах

CI-система читает результаты тестов из файлов отчётов. Фактически стандартный формат обмена - JUnit XML; его структура описана в статье Что такое система управления тестированием. Тестовые фреймворки записывают его по опции или через генератор отчётов (reporter). В Playwright:

typescript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  reporter: process.env.CI
    ? [['junit', { outputFile: 'results.xml' }], ['blob']]
    : 'html',
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

Опция forbidOnly делает прогон неуспешным, если по ошибке закоммичен тест с пометкой test.only. В pytest отчёт создаётся командой pytest --junit-xml=results.xml.

GitLab CI читает JUnit XML из ключа artifacts:reports:junit:

yaml
e2e:
  stage: test
  image: mcr.microsoft.com/playwright:v1.64.1-noble
  parallel: 4
  script:
    - npm ci
    - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
  artifacts:
    when: always
    paths:
      - results.xml
      - test-results/
    reports:
      junit: results.xml

when: always выгружает отчёт и тогда, когда задание упало, то есть именно тогда, когда он нужен. GitLab показывает результаты на вкладке «Tests» пайплайна и в сводке тестов запроса на слияние. В запросе на слияние он сравнивает результаты с целевой веткой и выделяет новые падения, существующие падения и исправленные падения. Документация задаёт ограничения: один файл JUnit должен быть меньше 30 МБ, все файлы задания - меньше 100 МБ, а из тестов с одинаковыми именами используется только первый.

Повторы упавших тестов

Пайплайн может автоматически перезапускать упавшие тесты: в Playwright опцией retries, в pytest плагином pytest-rerunfailures, в Maven свойством surefire.rerunFailingTestsCount. Тест, который прошёл при повторе, отмечается как нестабильный (flaky). Повторы обычно включают только в CI, как в конфигурации выше, с небольшим числом попыток.

Повтор превращает плавающее падение в прохождение, в том числе падение, вызванное настоящим дефектом. Поэтому статус flaky сохраняется в отчёте, а не сливается с passed. Maven Surefire записывает неуспешные попытки как элементы flakyFailure внутри элемента testcase; Playwright может сделать прогон неуспешным при любом нестабильном тесте с опцией --fail-on-flaky-tests. Причины, обнаружение и карантин нестабильных тестов описаны в статье Нестабильные (flaky) тесты.

Артефакты

Упавший тест на агенте CI нельзя отладить интерактивно: после окончания задания агента уже нет. Поэтому задание сохраняет артефакты (artifacts) - файлы, которые документируют запуск:

  • снимки экрана в момент падения;
  • трассировки с действиями, сетевыми запросами и снимками DOM (в Playwright trace: 'on-first-retry' записывает трассировку при первом повторе);
  • видео теста;
  • журналы приложения и тестового окружения;
  • сами файлы отчётов.

В GitHub Actions артефакты выгружаются действием actions/upload-artifact:

yaml
    - uses: actions/upload-artifact@v7
      if: ${{ !cancelled() }}
      with:
        name: results-${{ matrix.shardIndex }}
        path: |
          results.xml
          test-results/
        retention-days: 7

Условие ${{ !cancelled() }} выгружает артефакты и после упавших шагов. Срок хранения ограничивает объём: артефакты падения нужны днями, а не месяцами. GitLab умеет связать файл с упавшим тестом: строка [[ATTACHMENT|/path/to/file]] в system-out тест-кейса в JUnit XML появляется ссылкой в подробностях теста.

Результаты в тестовых прогонах системы управления тестированием

CI-система показывает результаты одного пайплайна. Система управления тестированием хранит их как тестовые прогоны рядом с ручным тестированием и во времени. Обычное соответствие:

  • один тестовый прогон на пайплайн: прогон называется по пайплайну и фиксирует коммит, ветку, номер сборки и окружение;
  • один результат на тест-кейс: каждый автотест связан с тест-кейсом идентификатором, который известен обеим сторонам;
  • статусы: failure, error и skipped из JUnit XML сопоставляются со статусами прогона, а нестабильные результаты получают отдельную отметку;
  • вложения: снимки экрана и трассировки прикладываются к результатам упавших тестов.

Идентификатор тест-кейса указывается в самом тесте. В Playwright это может быть аннотация:

typescript
import { test, expect } from '@playwright/test';

test('pay with saved card', {
  annotation: { type: 'test-case-id', description: 'TC-12' },
}, async ({ page }) => {
  await page.goto('https://example.com/checkout');
  await page.getByRole('button', { name: 'Pay with saved card' }).click();
  await expect(page.getByRole('heading', { name: 'Payment accepted' })).toBeVisible();
});

Генератор отчётов JUnit в Playwright записывает аннотации как свойства тест-кейса:

xml
<testcase name="pay with saved card" classname="checkout.spec.ts" time="1.62">
  <properties>
    <property name="test-case-id" value="TC-12"/>
  </properties>
</testcase>

В pytest то же делает фикстура record_property. Результаты попадают в систему управления тестированием либо как импортированный файл отчёта, либо через её API, который вызывает задание в конце пайплайна. Структура тестового прогона и статусы результатов описаны в статье Тестовый прогон.

Выбор тестов и анализ влияния изменений

Когда полный набор становится слишком долгим для каждого изменения, пайплайн запускает только тесты, которые затрагивает изменение. Анализ влияния изменений на тесты (test impact analysis) определяет, какое подмножество тестов запускать для данного набора изменений. Пол Хаммант на сайте Фаулера (2017) описывает обычный механизм: карту соответствия тестов и исходных файлов, которые они выполняют. Карту строят по покрытию кода, запуская каждый тест отдельно, хранят в системе контроля версий и обновляют средствами CI.

У карты есть пробелы. Изменения конфигурации, файлов ресурсов и данных не видны в покрытии кода, а новые тесты не попадают в карту, пока её не перестроят. Поэтому выбор тестов сочетают с полным запуском по расписанию. Более простая форма выбора встроена в некоторые средства запуска: опция Playwright --only-changed запускает только файлы тестов, изменённые относительно ссылки Git.

Окружения и тестовые данные в CI

Тесты в CI выполняются в окружении, которое готовит пайплайн или которое существует постоянно. Фаулер относит тестирование в копии продуктивного окружения к практикам CI. Распространённые подходы:

  • зависимости (база данных, очередь сообщений, заглушки внешних сервисов) запускаются в контейнерах на время задания, и каждый пайплайн получает чистое состояние;
  • развёрнутое тестовое окружение общее для пайплайнов, а запуски, которые его используют, ограничиваются группой параллельности; в GitHub Actions concurrency с cancel-in-progress: true отменяет более старый запуск в той же группе;
  • тестовые данные создают сами тесты или загружает пайплайн перед тестами, и каждый шард использует свои данные, чтобы параллельные задания не мешали друг другу;
  • секреты, например токены тестовых учётных записей, берутся из хранилища секретов CI-системы, а не из репозитория.

Окружение записывается вместе с результатами: версия сборки, адрес окружения, браузер и его версия. Без этого упавший результат нельзя воспроизвести.

CI, непрерывная поставка и непрерывное развёртывание

Таблица сравнивает три связанные практики.

Непрерывная интеграция Непрерывная поставка Непрерывное развёртывание
Цель Изменения часто интегрируются и проверяются Продукт можно выпустить в любой момент Каждое изменение выпускается автоматически
Конец пайплайна Проверенная сборка Кандидат в релиз, готовый к продакшену Изменение в продакшене
Решение о релизе Не входит в CI Принимают люди Принимает пайплайн
Роль автотестов Проверить каждое изменение Допустить кандидата в релиз Единственный шлюз перед продакшеном

Непрерывная поставка (continuous delivery) описана в книге Джеза Хамбла и Дэвида Фарли «Continuous Delivery» (2010). Фаулер определяет её как создание программного обеспечения так, чтобы его можно было выпустить в продакшен в любой момент. Непрерывное развёртывание (continuous deployment) идёт на шаг дальше: каждое изменение, прошедшее пайплайн, автоматически попадает в продакшен. Непрерывное развёртывание требует непрерывной поставки, и обе требуют CI.

Общий термин CI/CD охватывает весь путь от коммита до релиза. Для автотестов разница - в последствии прохождения: в CI оно разрешает слияние, при непрерывной поставке - релиз, при непрерывном развёртывании - выполняет его.

Термины

Термин Значение
Непрерывная интеграция (continuous integration) Практика, при которой изменения вливаются в общую основную ветку не реже раза в день, а каждую интеграцию проверяет автоматическая сборка с тестами
Пайплайн (pipeline) Последовательность заданий, которые CI-система выполняет для одного события: сборка, тесты, публикация, развёртывание
Шлюз качества (quality gate) Этап пайплайна, падение которого останавливает пайплайн
Шардирование (sharding) Разбиение набора тестов на части, которые выполняются как независимые задания на разных машинах
Артефакт (artifact) Файл, сохранённый заданием CI: отчёт, снимок экрана, трассировка, журнал
Анализ влияния изменений на тесты (test impact analysis) Техника, которая выбирает подмножество тестов для запуска при данном наборе изменений
Непрерывная поставка (continuous delivery) Практика, при которой продукт поддерживается в состоянии, пригодном для выпуска в продакшен в любой момент
Непрерывное развёртывание (continuous deployment) Практика, при которой каждое изменение, прошедшее пайплайн, автоматически выпускается в продакшен

Источники

Серия

Автоматизация тестирования
  1. Уровни автоматизации и пирамида тестов
  2. Локаторы в UI-автотестах
  3. Репозиторий объектов и Page Object
  4. Нестабильные (flaky) тесты
  5. Автотесты в CIТекущая

Статьи Wiki пишет и поддерживает команда ComplexQA.