GitLab CI/CD для фронтенд-тестов
GitLab CI/CD — один из самых распространённых инструментов автоматизации в компаниях, особенно в enterprise. Правильно настроенный pipeline ускоряет обратную связь, надёжно запускает тесты и даёт понятные отчёты.
Эта статья — про особенности GitLab CI/CD именно в контексте фронтенд-тестирования: unit, интеграционные и E2E.
Содержание
- Структура pipeline
- Stages: lint → typecheck → unit → build → e2e
- Cache vs artifacts
- Параллельный запуск unit-тестов
- E2E с Playwright в Docker
- Шардинг E2E в GitLab CI
- Services для баз данных и моков
- needs и DAG-зависимости
- Rules: когда запускать тесты
- JUnit-отчёты и coverage
- Безопасность: переменные и secrets
- Полный пример .gitlab-ci.yml
- Чеклист
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Структура pipeline
Классический фронтенд-pipeline выглядит так:
lint → typecheck → unit → build → e2e
| Stage | Что происходит | Время |
|---|---|---|
lint | ESLint, Prettier, commitlint | секунды |
typecheck | tsc --noEmit | секунды–минуты |
unit | Vitest/Jest + React Testing Library | минуты |
build | Сборка production-бандла | минуты |
e2e | Playwright/Cypress против сборки | минуты–десятки минут |
Стадии выполняются последовательно, но job’ы внутри одной стадии — параллельно.
Stages: lint → typecheck → unit → build → e2e
stages:
- lint
- typecheck
- unit
- build
- e2e
lint:
stage: lint
image: node:20-alpine
script:
- npm ci
- npm run lint
-typecheck:
stage: typecheck
image: node:20-alpine
script:
- npm ci
- npm run typecheck
unit:
stage: unit
image: node:20-alpine
script:
- npm ci
- npm run test:unit
artifacts:
reports:
junit: reports/unit-tests.xml
paths:
- coverage/
build:
stage: build
image: node:20-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
Cache vs artifacts
| Cache | Artifacts | |
|---|---|---|
| Назначение | Ускорение установки зависимостей | Передача файлов между job’ами |
| Пример | node_modules/, .npm/ | dist/, отчёты тестов |
| Жизнь | Между pipeline’ами | Внутри одного pipeline |
| Управление | cache: | artifacts: |
Пример cache
default:
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
policy: pull-push
Пример artifacts
build:
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 day
💡
cache— дляnode_modules.artifacts— дляdist/и отчётов. Не путай.
Параллельный запуск unit-тестов
Vitest и Jest умеют запускать тесты в несколько потоков. В GitLab CI важно правильно выделить ресурсы.
unit:
stage: unit
image: node:20-alpine
variables:
NODE_OPTIONS: "--max-old-space-size=4096"
script:
- npm ci
- npm run test:unit -- --reporter=junit --outputFile=reports/unit-tests.xml
parallel:
matrix:
- SHARD_INDEX: [1, 2, 3, 4]
SHARD_TOTAL: [4]
artifacts:
reports:
junit: reports/unit-tests.xml
Для Vitest шардинг настраивается через --shard=${SHARD_INDEX}/${SHARD_TOTAL}.
E2E с Playwright в Docker
Playwright предоставляет официальные образы со всеми браузерами и зависимостями.
e2e:
stage: e2e
image: mcr.microsoft.com/playwright:v1.46.0-jammy
script:
- npm ci
- npx playwright install-deps
- npm run build
- npm run test:e2e
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 3 days
⚠️ Образ
playwright:vX.Y.Z-jammyуже содержит браузеры.npx playwright installобычно не нужен, ноinstall-depsможет потребоваться для системных библиотек.
Шардинг E2E в GitLab CI
e2e:
stage: e2e
image: mcr.microsoft.com/playwright:v1.46.0-jammy
parallel:
matrix:
- SHARD_INDEX: [1, 2, 3]
SHARD_TOTAL: [3]
script:
- npm ci
- npm run build
- npx playwright test --shard=$SHARD_INDEX/$SHARD_TOTAL
artifacts:
when: always
paths:
- blob-reports/
expire_in: 1 day
merge-reports:
stage: e2e
image: mcr.microsoft.com/playwright:v1.46.0-jammy
needs:
- job: e2e
artifacts: true
script:
- npx playwright merge-reports --reporter html ./blob-reports
artifacts:
paths:
- playwright-report/
expire_in: 3 days
Services для баз данных и моков
Если E2E используют реальную БД, подключи её как service:
e2e-with-db:
stage: e2e
image: node:20-alpine
services:
- name: postgres:15-alpine
alias: postgres
variables:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: "postgresql://test:test@postgres:5432/test"
script:
- npm ci
- npm run db:migrate
- npm run db:seed
- npm run test:e2e
Services доступны по alias. Внутри job’ы хост postgres резолвится в контейнер PostgreSQL.
needs и DAG-зависимости
По умолчанию job’ы одной стадии ждут завершения всех job’ов предыдущей стадии. needs позволяет строить DAG и запускать job’ы раньше.
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
e2e-smoke:
stage: e2e
needs:
- job: build
artifacts: true
script:
- npx playwright test --grep @smoke
e2e-regression:
stage: e2e
needs:
- job: build
artifacts: true
script:
- npx playwright test --grep-invert @slow
build выполнится один раз, а оба E2E-job’а запустятся параллельно, как только сборка готова.
Rules: когда запускать тесты
Не все тесты нужны на каждое изменение.
lint:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
unit:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
e2e:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_LABELS =~ /run-e2e/'
when: always
Варианты условий
| Условие | Когда запускать |
|---|---|
CI_PIPELINE_SOURCE == "merge_request_event" | На каждый MR |
CI_COMMIT_BRANCH == "main" | После merge в main |
CI_MERGE_REQUEST_LABELS =~ /run-e2e/ | Только если на MR повесили label |
CI_COMMIT_MESSAGE =~ /\[skip ci\]/ | Не запускать (стандартное соглашение) |
JUnit-отчёты и coverage
GitLab умеет парсить JUnit XML и показывать упавшие тесты прямо в MR.
Vitest → JUnit
// vitest.config.ts
export default defineConfig({
test: {
reporters: ["default", "junit"],
outputFile: {
junit: "./reports/unit-tests.xml",
},
},
});
Playwright → JUnit
// playwright.config.ts
export default defineConfig({
reporter: [
["html"],
["junit", { outputFile: "reports/e2e-results.xml" }],
],
});
Coverage
unit:
script:
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
paths:
- coverage/
💡 Регулярка для coverage зависит от формата вывода Vitest. Проверь вывод
npm run test:unit -- --coverage.
Безопасность: переменные и secrets
- Храни secrets в CI/CD Variables (Settings → CI/CD → Variables).
- Для production используй Protected variables.
- Никогда не коммить
.envфайлы с реальными ключами. - Для E2E используй отдельные тестовые аккаунты, не продовые.
variables:
TEST_USER_EMAIL: $TEST_USER_EMAIL
TEST_USER_PASSWORD: $TEST_USER_PASSWORD
Полный пример .gitlab-ci.yml
stages:
- lint
- typecheck
- unit
- build
- e2e
default:
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
variables:
NODE_OPTIONS: "--max-old-space-size=4096"
FF_USE_FASTZIP: "true"
lint:
stage: lint
script:
- npm ci --prefer-offline
- npm run lint
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
typecheck:
stage: typecheck
script:
- npm ci --prefer-offline
- npm run typecheck
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
unit:
stage: unit
script:
- npm ci --prefer-offline
- npm run test:unit -- --reporter=junit --outputFile=reports/unit-tests.xml
artifacts:
when: always
reports:
junit: reports/unit-tests.xml
paths:
- coverage/
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
build:
stage: build
script:
- npm ci --prefer-offline
- npm run build
artifacts:
paths:
- dist/
- package.json
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
e2e-smoke:
stage: e2e
image: mcr.microsoft.com/playwright:v1.46.0-jammy
needs:
- job: build
artifacts: true
script:
- npm ci --prefer-offline
- npx playwright install-deps
- npx playwright test --grep @smoke
artifacts:
when: always
paths:
- playwright-report/
- test-results/
reports:
junit: reports/e2e-results.xml
expire_in: 3 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
e2e-regression:
stage: e2e
image: mcr.microsoft.com/playwright:v1.46.0-jammy
needs:
- job: build
artifacts: true
parallel:
matrix:
- SHARD_INDEX: [1, 2, 3]
SHARD_TOTAL: [3]
script:
- npm ci --prefer-offline
- npx playwright install-deps
- npx playwright test --shard=$SHARD_INDEX/$SHARD_TOTAL --grep-invert @slow
artifacts:
when: always
paths:
- blob-reports/
expire_in: 1 day
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_LABELS =~ /run-e2e/'
Чеклист
- Pipeline разбит на stages: lint → typecheck → unit → build → e2e.
-
node_modulesкешируется между pipeline’ами. - Production-сборка передаётся в E2E через artifacts.
- Unit-тесты генерируют JUnit-отчёты.
- E2E запускаются в официальном Docker-образе Playwright.
- E2E шардируются для ускорения.
- Используются
needsдля параллельного запуска независимых job’ов. -
rulesзапускают тяжёлые E2E не на каждый MR, а по необходимости. - Secrets хранятся в CI/CD Variables, не в репозитории.
- Coverage и отчёты о тестах видны в MR.
Ключевые тезисы для интервью
- Фронтенд-pipeline строится из стадий
lint → typecheck → unit → build → e2e; stages выполняются последовательно, а job’ы внутри stage — параллельно, что позволяет ускорить выполнение без потери порядка проверок. cacheускоряет установку зависимостей между pipeline’ами (например,node_modules), аartifactsпередаёт файлы между job’ами внутри одного pipeline (например, сборку для E2E-тестов).- Параллелизация unit-тестов через
parallel: matrixс переменнымиSHARD_INDEXиSHARD_TOTALраспределяет нагрузку; шардинг E2E требует отдельного job’а дляmerge-reports, который собирает результаты изblob-reports. - Playwright запускают в официальном Docker-образе с предустановленными браузерами и системными зависимостями;
servicesподнимают базы данных или моки рядом с job’ом, обращаясь к ним по alias. needsстроит DAG-зависимости, позволяя job’ам запускаться раньше без ожидания всей предыдущей stage;rulesуправляют условным запуском, например, тяжёлые E2E только вmainили по label MR.- JUnit-отчёты и регулярное выражение coverage делают результаты тестов видимыми в MR; это улучшает обратную связь и помогает быстро выявлять проблемы.
- Secrets хранят в CI/CD Variables (желательно protected), никогда не коммитят
.envв репозиторий; полный.gitlab-ci.ymlкомбинирует кэш, artifacts,needs, шардинг,rulesи отчёты в единый pipeline.
Заключение
Правильно настроенный GitLab CI/CD pipeline превращает запуск тестов из рутины в быструю и предсказуемую проверку каждого изменения. Главные рычаги — разбиение на stages, кэш node_modules, передача сборки через artifacts, параллельный запуск через needs и шардинг, а также точные rules, чтобы не гонять тяжёлые E2E без нужды. Не забывайте о JUnit-отчётах, coverage и хранении secrets в CI/CD Variables. Попробуйте применить чеклист к своему проекту и убедитесь, что pipeline даёт обратную связь за минуты, а не часы.