CI/CD для QA Engineers: От Manual Testing к Continuous Testing

Полное руководство по Jenkins, GitHub Actions, Docker и интеграции test automation в CI/CD pipeline


CI/CD Pipeline

📍 Вы здесь:

[✓] Статья 1: Основы QA
[✓] Статья 2: Практика QA  
[✓] Статья 3: DSA для QA
[✓] Статья 4: Automation Frameworks
[→] Статья 5: CI/CD ← Сейчас читаете
[ ] Статья 6: Performance Testing
[ ] Статья 7: Работа в Apple

Прогресс: 71% ✨

“У нас есть 500 автоматизированных тестов, но мы запускаем их вручную раз в неделю” — услышав это на собеседовании, я понял, что компания застряла в 2015 году.

В 2026 году continuous testing — это не опция, а необходимость. Apple, Google, Amazon выкатывают изменения десятки раз в день. Как они это делают? CI/CD pipelines с интегрированным тестированием.

Из реальной вакансии Apple:

“Experience with CI/CD pipelines and test automation integration”

В этой статье вы научитесь:

  • ✅ Настраивать Jenkins для test automation
  • ✅ Создавать GitHub Actions workflows
  • ✅ Контейнеризировать тесты с Docker
  • ✅ Интегрировать Playwright/Selenium в pipeline
  • ✅ Настраивать notifications (Slack, Teams)
  • ✅ Создавать test dashboards

К концу статьи у вас будет production-ready CI/CD pipeline для вашего портфолио.


📋 Содержание

  1. Что такое CI/CD и почему это важно для QA
  2. Git & GitHub для QA
  3. GitHub Actions: Автоматизация с нуля
  4. Jenkins: Enterprise-level CI/CD
  5. Docker для Test Automation
  6. Интеграция тестов в Pipeline
  7. Test Reporting & Dashboards
  8. Notifications & Monitoring
  9. Реальный проект: Complete CI/CD Setup
  10. Ресурсы для изучения

🎯 Что такое CI/CD и почему это важно для QA?

Определения

CI (Continuous Integration):

  • Разработчики часто коммитят код (несколько раз в день)
  • Каждый коммит автоматически собирается
  • Автоматические тесты запускаются при каждом коммите
  • Быстрая обратная связь (< 10 минут)

CD (Continuous Delivery/Deployment):

  • Автоматическая доставка в staging/production
  • После прохождения всех тестов
  • Минимальное ручное вмешательство

Традиционный процесс (без CI/CD)

День 1: Разработчик пишет код
День 2-3: Код лежит в ветке
День 4: Создается Pull Request
День 5: Code review
День 6: Merge в main
День 7: QA вручную запускает тесты
День 8: Находятся баги
День 9: Исправления
День 10: Релиз

Результат: 10 дней, много ручной работы, поздние находки багов

С CI/CD

Минута 0: Разработчик пушит код
Минута 1: CI автоматически:
  - Собирает проект
  - Запускает unit tests
  - Запускает integration tests
  - Запускает E2E tests
Минута 10: Результаты готовы
  ✅ Все тесты прошли → автоматический deploy на staging
  ❌ Тесты упали → уведомление в Slack
  
Результат: 10 минут, автоматизация, немедленная обратная связь

Роль QA в CI/CD

Традиционная роль QA:

  • ❌ Ждет готовые билды
  • ❌ Запускает тесты вручную
  • ❌ Ищет баги после разработки

Modern QA Engineer (SDET):

  • ✅ Создает автоматизированные тесты
  • ✅ Интегрирует тесты в CI/CD
  • ✅ Мониторит test stability
  • ✅ Shift-left testing (тестирование на ранних стадиях)

🌳 Git & GitHub для QA

Базовые команды Git

Setup:

# Настройка пользователя
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

# Инициализация репозитория
git init

# Клонирование существующего
git clone https://github.com/username/repo.git

Ежедневная работа:

# Проверка статуса
git status

# Добавление файлов
git add .                    # Все файлы
git add tests/login.spec.js  # Конкретный файл

# Коммит
git commit -m "Add login tests"

# Отправка на GitHub
git push origin main

# Получение изменений
git pull origin main

Работа с ветками:

# Создание новой ветки
git checkout -b feature/add-payment-tests

# Переключение между ветками
git checkout main
git checkout feature/add-payment-tests

# Список веток
git branch

# Удаление ветки
git branch -d feature/add-payment-tests

# Слияние ветки
git checkout main
git merge feature/add-payment-tests

Git Workflow для QA

Feature Branch Workflow:

# 1. Создаем ветку для новых тестов
git checkout -b feature/checkout-tests

# 2. Пишем тесты
# tests/checkout.spec.js

# 3. Коммитим часто
git add tests/checkout.spec.js
git commit -m "Add checkout validation tests"

# 4. Пушим в GitHub
git push origin feature/checkout-tests

# 5. Создаем Pull Request на GitHub

# 6. После review - merge в main

Commit Message Best Practices:

# ❌ Плохо
git commit -m "updates"
git commit -m "fix"
git commit -m "test"

# ✅ Хорошо
git commit -m "Add login tests for valid/invalid credentials"
git commit -m "Fix flaky test in checkout flow"
git commit -m "Update test data for payment methods"
git commit -m "Refactor Page Objects to use fixtures"

Conventional Commits для QA:

# Формат: <type>: <description>

git commit -m "test: add E2E tests for user registration"
git commit -m "fix: resolve timeout issue in API tests"
git commit -m "refactor: improve Page Object structure"
git commit -m "docs: update README with test execution guide"
git commit -m "chore: update Playwright to v1.40"

.gitignore для Test Projects

.gitignore:

# Node modules
node_modules/
package-lock.json

# Test results
test-results/
playwright-report/
screenshots/
videos/
logs/
allure-results/
allure-report/

# Environment variables
.env
.env.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Test data
test-data/local/

⚡ GitHub Actions: Автоматизация с нуля

Почему GitHub Actions?

Преимущества:

  • ✅ Бесплатно для публичных репозиториев
  • ✅ 2000 минут/месяц для приватных (free tier)
  • ✅ Интеграция с GitHub из коробки
  • ✅ Огромный marketplace workflows
  • ✅ Простой YAML синтаксис

Базовая структура

.github/workflows/tests.yml:

name: Automated Tests

# Когда запускать
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    # Запуск каждый день в 9:00 UTC
    - cron: '0 9 * * *'
  workflow_dispatch: # Ручной запуск

# Что запускать
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test

Playwright Tests в GitHub Actions

.github/workflows/playwright.yml:

name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    
    - name: Run Playwright tests
      run: npx playwright test
    
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30
    
    - name: Upload screenshots
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: screenshots
        path: screenshots/

Parallel Testing Matrix

Запуск тестов на разных браузерах параллельно:

name: Cross-Browser Testing

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        node-version: [16, 18, 20]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright
      run: npx playwright install --with-deps ${{ matrix.browser }}
    
    - name: Run tests on ${{ matrix.browser }}
      run: npx playwright test --project=${{ matrix.browser }}

Sharding для быстрого выполнения

Разделение тестов на части:

name: Sharded Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    
    steps:
    - uses: actions/checkout@v3
    
    - uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright
      run: npx playwright install --with-deps
    
    - name: Run shard ${{ matrix.shard }} of 4
      run: npx playwright test --shard=${{ matrix.shard }}/4

Environment Secrets

Работа с секретами:

name: Tests with Secrets

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Run tests
      env:
        API_KEY: ${{ secrets.API_KEY }}
        DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        BASE_URL: ${{ secrets.BASE_URL }}
      run: npm test

Добавление секретов в GitHub:

  1. Repo → Settings → Secrets and variables → Actions
  2. New repository secret
  3. Name: API_KEY, Value: your-secret-key

Deploy после успешных тестов

Complete pipeline:

name: Test and Deploy

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install and test
      run: |
        npm ci
        npm test
    
    # Только если тесты прошли
  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: success()
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to staging
      run: |
        echo "Deploying to staging..."
        # Your deploy script
    
    - name: Run smoke tests on staging
      run: npm run test:smoke

Notifications

Slack notification:

name: Tests with Notifications

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    - run: npm ci && npm test
    
    - name: Slack Notification on Success
      if: success()
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_CHANNEL: qa-notifications
        SLACK_COLOR: good
        SLACK_MESSAGE: '✅ All tests passed!'
        SLACK_TITLE: Test Results
    
    - name: Slack Notification on Failure
      if: failure()
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_CHANNEL: qa-notifications
        SLACK_COLOR: danger
        SLACK_MESSAGE: '❌ Tests failed! Check logs.'
        SLACK_TITLE: Test Results

🔧 Jenkins: Enterprise-level CI/CD

Почему Jenkins?

Когда использовать Jenkins:

  • ✅ Enterprise environments
  • ✅ On-premise infrastructure
  • ✅ Сложные pipelines с множеством стадий
  • ✅ Интеграция с legacy системами
  • ✅ Большие команды

Когда использовать GitHub Actions:

  • ✅ Small to medium teams
  • ✅ Cloud-native approach
  • ✅ GitHub-centric workflow
  • ✅ Простые pipelines

Jenkins Installation

Docker способ (самый простой):

# Скачиваем и запускаем Jenkins
docker run -d \
  --name jenkins \
  -p 8080:8080 \
  -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  jenkins/jenkins:lts

# Получаем initial admin password
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

# Открываем http://localhost:8080

После установки:

  1. Вставляем admin password
  2. Install suggested plugins
  3. Create admin user
  4. Start using Jenkins!

Jenkinsfile для Playwright

Declarative Pipeline:

pipeline {
    agent any
    
    environment {
        BASE_URL = 'https://staging.example.com'
    }
    
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main',
                    url: 'https://github.com/username/test-automation.git'
            }
        }
        
        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
                sh 'npx playwright install --with-deps'
            }
        }
        
        stage('Run Tests') {
            steps {
                sh 'npx playwright test'
            }
        }
        
        stage('Publish Results') {
            steps {
                publishHTML([
                    reportName: 'Playwright Report',
                    reportDir: 'playwright-report',
                    reportFiles: 'index.html',
                    keepAll: true,
                    alwaysLinkToLastBuild: true
                ])
            }
        }
    }
    
    post {
        always {
            junit 'test-results/*.xml'
            archiveArtifacts artifacts: 'screenshots/**/*.png',
                           allowEmptyArchive: true
        }
        
        failure {
            emailext(
                subject: "❌ Tests Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: """
                    Build failed!
                    
                    Job: ${env.JOB_NAME}
                    Build: ${env.BUILD_NUMBER}
                    URL: ${env.BUILD_URL}
                """,
                to: 'qa-team@example.com'
            )
        }
        
        success {
            echo '✅ All tests passed!'
        }
    }
}

Многоступенчатый Pipeline

Комплексный pipeline с разными типами тестов:

pipeline {
    agent any
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm ci'
            }
        }
        
        stage('Unit Tests') {
            steps {
                sh 'npm run test:unit'
            }
        }
        
        stage('Integration Tests') {
            parallel {
                stage('API Tests') {
                    steps {
                        sh 'npm run test:api'
                    }
                }
                
                stage('Component Tests') {
                    steps {
                        sh 'npm run test:component'
                    }
                }
            }
        }
        
        stage('E2E Tests') {
            steps {
                sh 'npx playwright test'
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'main'
                expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
            }
            steps {
                sh './deploy-staging.sh'
            }
        }
        
        stage('Smoke Tests on Staging') {
            when {
                branch 'main'
            }
            steps {
                sh 'npm run test:smoke'
            }
        }
    }
    
    post {
        always {
            publishHTML([
                reportName: 'Test Report',
                reportDir: 'playwright-report',
                reportFiles: 'index.html'
            ])
            
            junit 'test-results/**/*.xml'
        }
        
        failure {
            slackSend(
                channel: '#qa-notifications',
                color: 'danger',
                message: "❌ Build Failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
            )
        }
        
        success {
            slackSend(
                channel: '#qa-notifications',
                color: 'good',
                message: "✅ Build Successful: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
            )
        }
    }
}

Jenkins Plugins для QA

Essential Plugins:

# Установка через Jenkins UI:
# Manage Jenkins → Manage Plugins → Available

1. HTML Publisher Plugin       # Test reports
2. JUnit Plugin                # Test results
3. Slack Notification Plugin   # Notifications
4. Blue Ocean                  # Modern UI
5. Pipeline                    # Pipeline support
6. Git Plugin                  # Git integration
7. Docker Plugin               # Docker integration
8. Allure Plugin              # Beautiful reports

Scheduled Tests (Cron)

Запуск тестов по расписанию:

pipeline {
    agent any
    
    triggers {
        // Каждый день в 9:00
        cron('0 9 * * *')
        
        // Каждый час
        // cron('0 * * * *')
        
        // Каждый понедельник в 8:00
        // cron('0 8 * * 1')
    }
    
    stages {
        stage('Nightly Tests') {
            steps {
                sh 'npx playwright test --project=full-suite'
            }
        }
    }
}

🐳 Docker для Test Automation

Почему Docker для тестов?

Проблемы без Docker:

  • ❌ “Works on my machine”
  • ❌ Зависимость от environment
  • ❌ Сложная настройка CI
  • ❌ Несогласованные версии браузеров

С Docker:

  • ✅ Consistent environment
  • ✅ Easy setup
  • ✅ Изолированное выполнение
  • ✅ Версионирование окружения

Dockerfile для Playwright

Dockerfile:

FROM mcr.microsoft.com/playwright:v1.40.0-focal

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy test files
COPY . .

# Run tests by default
CMD ["npx", "playwright", "test"]

docker-compose.yml:

version: '3.8'

services:
  tests:
    build: .
    environment:
      - BASE_URL=https://staging.example.com
      - CI=true
    volumes:
      - ./test-results:/app/test-results
      - ./playwright-report:/app/playwright-report
      - ./screenshots:/app/screenshots

Запуск тестов:

# Build image
docker build -t my-tests .

# Run tests
docker run --rm my-tests

# Run with docker-compose
docker-compose up --abort-on-container-exit

# Run specific test
docker run --rm my-tests npx playwright test tests/login.spec.js

Multi-stage Dockerfile (оптимизация)

# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --production

# Stage 2: Test dependencies
FROM mcr.microsoft.com/playwright:v1.40.0-focal AS test-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Stage 3: Runtime
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app

# Copy dependencies
COPY --from=test-deps /app/node_modules ./node_modules
COPY . .

# Run tests
CMD ["npx", "playwright", "test"]

Docker в Jenkins

Jenkinsfile с Docker:

pipeline {
    agent {
        docker {
            image 'mcr.microsoft.com/playwright:v1.40.0-focal'
            args '-v $HOME/.npm:/root/.npm'
        }
    }
    
    stages {
        stage('Test') {
            steps {
                sh 'npm ci'
                sh 'npx playwright test'
            }
        }
    }
}

Docker Compose для интеграционных тестов

docker-compose.test.yml:

version: '3.8'

services:
  # Ваше приложение
  app:
    build: ../app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test
      - DB_HOST=db
    depends_on:
      - db
  
  # Database
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    ports:
      - "5432:5432"
  
  # Тесты
  tests:
    build: .
    environment:
      - BASE_URL=http://app:3000
      - DB_URL=postgresql://testuser:testpass@db:5432/testdb
    depends_on:
      - app
      - db
    command: sh -c "sleep 5 && npx playwright test"

Запуск:

docker-compose -f docker-compose.test.yml up --abort-on-container-exit

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

Test Pyramid в CI/CD

Pipeline Stages:
┌─────────────────────────────────────────┐
│  1. Unit Tests (30 сек)                │
│     ✓ Fast                             │
│     ✓ No dependencies                  │
│     ✓ Run on every commit              │
├─────────────────────────────────────────┤
│  2. Integration Tests (2-5 мин)        │
│     ✓ API tests                        │
│     ✓ Component tests                  │
│     ✓ Database tests                   │
├─────────────────────────────────────────┤
│  3. E2E Tests (10-15 мин)              │
│     ✓ Critical user flows              │
│     ✓ Smoke tests                      │
│     ✓ Visual regression                │
├─────────────────────────────────────────┤
│  4. Full Regression (30-60 мин)        │
│     ✓ Nightly builds only              │
│     ✓ All test suites                  │
│     ✓ Multiple environments            │
└─────────────────────────────────────────┘

Параллелизация тестов

package.json scripts:

{
  "scripts": {
    "test": "playwright test",
    "test:unit": "jest",
    "test:api": "newman run postman-collection.json",
    "test:e2e": "playwright test tests/e2e",
    "test:smoke": "playwright test tests/smoke --grep @smoke",
    "test:parallel": "playwright test --workers=4",
    "test:headed": "playwright test --headed",
    "test:debug": "playwright test --debug"
  }
}

GitHub Actions с параллелизацией:

name: Parallel Tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        suite: [smoke, auth, checkout, products, admin]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run ${{ matrix.suite }} tests
      run: npx playwright test tests/${{ matrix.suite }}

Conditional Testing

Запуск тестов в зависимости от изменений:

name: Smart Testing

on: [push]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    
    steps:
    - uses: actions/checkout@v3
    
    - uses: dorny/paths-filter@v2
      id: filter
      with:
        filters: |
          frontend:
            - 'src/frontend/**'
            - 'tests/e2e/**'
          backend:
            - 'src/backend/**'
            - 'tests/api/**'
  
  test-frontend:
    needs: detect-changes
    if: needs.detect-changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - run: npm ci && npm run test:e2e
  
  test-backend:
    needs: detect-changes
    if: needs.detect-changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - run: npm ci && npm run test:api

📊 Test Reporting & Dashboards

Playwright HTML Reporter

playwright.config.js:

export default {
    reporter: [
        ['html', { 
            outputFolder: 'playwright-report',
            open: 'never' 
        }],
        ['json', { 
            outputFile: 'test-results.json' 
        }],
        ['junit', { 
            outputFile: 'junit-results.xml' 
        }],
        ['list'] // Console output
    ],
};

Viewing reports в CI:

# GitHub Actions
- name: Upload HTML report
  uses: actions/upload-artifact@v3
  if: always()
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 30

Allure Reports

Самые красивые отчеты!

Installation:

npm install --save-dev @playwright/test allure-playwright

playwright.config.js:

export default {
    reporter: [
        ['allure-playwright', {
            detail: true,
            outputFolder: 'allure-results',
            suiteTitle: false
        }]
    ],
};

Генерация отчета:

# Установка Allure CLI
npm install -g allure-commandline

# Запуск тестов
npx playwright test

# Генерация отчета
allure generate allure-results -o allure-report --clean

# Открытие отчета
allure open allure-report

В CI:

# GitHub Actions
- name: Run tests
  run: npx playwright test

- name: Generate Allure Report
  if: always()
  run: |
    npm install -g allure-commandline
    allure generate allure-results -o allure-report --clean

- name: Deploy report to GitHub Pages
  if: always()
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: ./allure-report

Custom Test Dashboard

dashboard.js - Simple Dashboard:

const fs = require('fs');
const path = require('path');

class TestDashboard {
    constructor() {
        this.results = [];
    }

    parseTestResults(resultsPath) {
        const data = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
        
        const stats = {
            total: 0,
            passed: 0,
            failed: 0,
            skipped: 0,
            duration: 0,
            tests: []
        };

        data.suites.forEach(suite => {
            suite.specs.forEach(spec => {
                spec.tests.forEach(test => {
                    stats.total++;
                    stats.duration += test.results[0].duration;

                    if (test.results[0].status === 'passed') {
                        stats.passed++;
                    } else if (test.results[0].status === 'failed') {
                        stats.failed++;
                    } else {
                        stats.skipped++;
                    }

                    stats.tests.push({
                        name: spec.title,
                        suite: suite.title,
                        status: test.results[0].status,
                        duration: test.results[0].duration,
                        error: test.results[0].error?.message
                    });
                });
            });
        });

        return stats;
    }

    generateHTML(stats) {
        const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
        
        return `
<!DOCTYPE html>
<html>
<head>
    <title>Test Dashboard</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .stats {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 20px;
            margin-bottom: 30px;
        }
        .stat-card {
            padding: 20px;
            border-radius: 8px;
            text-align: center;
        }
        .stat-card.total { background: #e3f2fd; }
        .stat-card.passed { background: #c8e6c9; }
        .stat-card.failed { background: #ffcdd2; }
        .stat-card.skipped { background: #fff9c4; }
        .stat-value {
            font-size: 48px;
            font-weight: bold;
            margin: 10px 0;
        }
        .stat-label {
            font-size: 14px;
            color: #666;
            text-transform: uppercase;
        }
        .progress-bar {
            width: 100%;
            height: 30px;
            background: #e0e0e0;
            border-radius: 15px;
            overflow: hidden;
            margin: 20px 0;
        }
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #4caf50, #8bc34a);
            text-align: center;
            line-height: 30px;
            color: white;
            font-weight: bold;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th {
            background: #f5f5f5;
            font-weight: bold;
        }
        .status {
            padding: 4px 12px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
        }
        .status.passed { background: #c8e6c9; color: #2e7d32; }
        .status.failed { background: #ffcdd2; color: #c62828; }
        .status.skipped { background: #fff9c4; color: #f57f17; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Test Execution Dashboard</h1>
        <p>Generated: ${new Date().toLocaleString()}</p>

        <div class="stats">
            <div class="stat-card total">
                <div class="stat-label">Total Tests</div>
                <div class="stat-value">${stats.total}</div>
            </div>
            <div class="stat-card passed">
                <div class="stat-label">Passed</div>
                <div class="stat-value">${stats.passed}</div>
            </div>
            <div class="stat-card failed">
                <div class="stat-label">Failed</div>
                <div class="stat-value">${stats.failed}</div>
            </div>
            <div class="stat-card skipped">
                <div class="stat-label">Skipped</div>
                <div class="stat-value">${stats.skipped}</div>
            </div>
        </div>

        <div class="progress-bar">
            <div class="progress-fill" style="width: ${passRate}%">
                ${passRate}% Pass Rate
            </div>
        </div>

        <h2>Test Results</h2>
        <table>
            <thead>
                <tr>
                    <th>Suite</th>
                    <th>Test</th>
                    <th>Status</th>
                    <th>Duration</th>
                    <th>Error</th>
                </tr>
            </thead>
            <tbody>
                ${stats.tests.map(test => `
                    <tr>
                        <td>${test.suite}</td>
                        <td>${test.name}</td>
                        <td><span class="status ${test.status}">${test.status}</span></td>
                        <td>${(test.duration / 1000).toFixed(2)}s</td>
                        <td>${test.error || '-'}</td>
                    </tr>
                `).join('')}
            </tbody>
        </table>
    </div>
</body>
</html>
        `;
    }

    generate(resultsPath, outputPath) {
        const stats = this.parseTestResults(resultsPath);
        const html = this.generateHTML(stats);
        
        fs.writeFileSync(outputPath, html);
        console.log(\`Dashboard generated: \${outputPath}\`);
        
        return stats;
    }
}

// Usage
const dashboard = new TestDashboard();
dashboard.generate('test-results.json', 'dashboard.html');

В package.json:

{
  "scripts": {
    "test:report": "npx playwright test && node dashboard.js"
  }
}

🔔 Notifications & Monitoring

Slack Integration

Установка Slack App:

  1. Создайте Slack App: https://api.slack.com/apps
  2. Enable “Incoming Webhooks”
  3. Скопируйте Webhook URL

slack-notifier.js:

const axios = require('axios');

class SlackNotifier {
    constructor(webhookUrl) {
        this.webhookUrl = webhookUrl;
    }

    async sendTestResults(stats, buildUrl) {
        const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
        const color = stats.failed === 0 ? 'good' : 'danger';
        
        const message = {
            text: stats.failed === 0 
                ? '✅ All tests passed!' 
                : `❌ ${stats.failed} test(s) failed`,
            
            attachments: [{
                color: color,
                title: 'Test Execution Report',
                fields: [
                    {
                        title: 'Total Tests',
                        value: stats.total.toString(),
                        short: true
                    },
                    {
                        title: 'Passed',
                        value: `✅ ${stats.passed}`,
                        short: true
                    },
                    {
                        title: 'Failed',
                        value: `❌ ${stats.failed}`,
                        short: true
                    },
                    {
                        title: 'Pass Rate',
                        value: `${passRate}%`,
                        short: true
                    },
                    {
                        title: 'Duration',
                        value: `${(stats.duration / 1000 / 60).toFixed(1)} min`,
                        short: true
                    }
                ],
                footer: 'Test Automation',
                footer_icon: 'https://playwright.dev/img/playwright-logo.svg',
                ts: Math.floor(Date.now() / 1000)
            }]
        };

        if (buildUrl) {
            message.attachments[0].actions = [{
                type: 'button',
                text: 'View Details',
                url: buildUrl
            }];
        }

        try {
            await axios.post(this.webhookUrl, message);
            console.log('✅ Slack notification sent');
        } catch (error) {
            console.error('❌ Failed to send Slack notification:', error.message);
        }
    }

    async sendFailedTests(failedTests) {
        if (failedTests.length === 0) return;

        const message = {
            text: '❌ Failed Tests Details',
            attachments: failedTests.map(test => ({
                color: 'danger',
                title: test.name,
                text: test.error || 'No error message',
                fields: [
                    {
                        title: 'Suite',
                        value: test.suite,
                        short: true
                    },
                    {
                        title: 'Duration',
                        value: `${(test.duration / 1000).toFixed(2)}s`,
                        short: true
                    }
                ]
            }))
        };

        try {
            await axios.post(this.webhookUrl, message);
        } catch (error) {
            console.error('Failed to send details:', error.message);
        }
    }
}

module.exports = SlackNotifier;

В CI:

# GitHub Actions
- name: Send Slack Notification
  if: always()
  run: |
    node scripts/slack-notifier.js
  env:
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

Email Notifications

email-notifier.js:

const nodemailer = require('nodemailer');

class EmailNotifier {
    constructor(config) {
        this.transporter = nodemailer.createTransport({
            host: config.smtp.host,
            port: config.smtp.port,
            secure: true,
            auth: {
                user: config.smtp.user,
                pass: config.smtp.password
            }
        });
    }

    async sendTestResults(stats, recipients) {
        const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
        const subject = stats.failed === 0 
            ? `✅ All Tests Passed (${passRate}%)` 
            : `❌ ${stats.failed} Tests Failed`;

        const html = `
            <h2>Test Execution Report</h2>
            <table style="border-collapse: collapse;">
                <tr>
                    <td style="padding: 10px; border: 1px solid #ddd;"><strong>Total Tests:</strong></td>
                    <td style="padding: 10px; border: 1px solid #ddd;">${stats.total}</td>
                </tr>
                <tr>
                    <td style="padding: 10px; border: 1px solid #ddd;"><strong>Passed:</strong></td>
                    <td style="padding: 10px; border: 1px solid #ddd; color: green;">✅ ${stats.passed}</td>
                </tr>
                <tr>
                    <td style="padding: 10px; border: 1px solid #ddd;"><strong>Failed:</strong></td>
                    <td style="padding: 10px; border: 1px solid #ddd; color: red;">❌ ${stats.failed}</td>
                </tr>
                <tr>
                    <td style="padding: 10px; border: 1px solid #ddd;"><strong>Pass Rate:</strong></td>
                    <td style="padding: 10px; border: 1px solid #ddd;">${passRate}%</td>
                </tr>
            </table>
            
            ${stats.failed > 0 ? `
                <h3>Failed Tests:</h3>
                <ul>
                    ${stats.tests
                        .filter(t => t.status === 'failed')
                        .map(t => `<li>${t.suite} - ${t.name}</li>`)
                        .join('')}
                </ul>
            ` : ''}
        `;

        await this.transporter.sendMail({
            from: '"Test Automation" <noreply@example.com>',
            to: recipients.join(', '),
            subject: subject,
            html: html
        });
    }
}

module.exports = EmailNotifier;

Microsoft Teams Integration

teams-notifier.js:

const axios = require('axios');

class TeamsNotifier {
    constructor(webhookUrl) {
        this.webhookUrl = webhookUrl;
    }

    async sendTestResults(stats, buildUrl) {
        const passRate = ((stats.passed / stats.total) * 100).toFixed(1);
        const themeColor = stats.failed === 0 ? '00FF00' : 'FF0000';

        const card = {
            "@type": "MessageCard",
            "@context": "https://schema.org/extensions",
            "summary": "Test Results",
            "themeColor": themeColor,
            "title": stats.failed === 0 
                ? "✅ All Tests Passed" 
                : `❌ ${stats.failed} Tests Failed`,
            "sections": [{
                "activityTitle": "Test Execution Report",
                "facts": [
                    { "name": "Total Tests:", "value": stats.total },
                    { "name": "Passed:", "value": `✅ ${stats.passed}` },
                    { "name": "Failed:", "value": `❌ ${stats.failed}` },
                    { "name": "Pass Rate:", "value": `${passRate}%` },
                    { "name": "Duration:", "value": `${(stats.duration / 60000).toFixed(1)} min` }
                ]
            }]
        };

        if (buildUrl) {
            card.potentialAction = [{
                "@type": "OpenUri",
                "name": "View Report",
                "targets": [{ "os": "default", "uri": buildUrl }]
            }];
        }

        await axios.post(this.webhookUrl, card);
    }
}

module.exports = TeamsNotifier;

🎯 Реальный проект: Complete CI/CD Setup

Project Structure

ecommerce-ci-cd/
├── .github/
│   └── workflows/
│       ├── pr-tests.yml          # PR validation
│       ├── nightly-tests.yml     # Full regression
│       └── deploy.yml            # Deploy pipeline
├── tests/
│   ├── e2e/
│   ├── api/
│   └── smoke/
├── scripts/
│   ├── slack-notifier.js
│   ├── dashboard.js
│   └── cleanup.js
├── Dockerfile
├── docker-compose.yml
├── Jenkinsfile
├── playwright.config.js
└── package.json

Complete GitHub Actions Workflow

.github/workflows/complete-pipeline.yml:

name: Complete CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * *'  # Nightly at midnight
  workflow_dispatch:

env:
  NODE_VERSION: '18'

jobs:
  # Job 1: Lint & Static Analysis
  lint:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run ESLint
        run: npm run lint
      
      - name: Check code formatting
        run: npm run format:check

  # Job 2: Unit Tests
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  # Job 3: API Tests
  api-tests:
    name: API Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run API tests
        run: npm run test:api
        env:
          API_URL: ${{ secrets.API_URL }}
          API_KEY: ${{ secrets.API_KEY }}

  # Job 4: E2E Tests (Sharded)
  e2e-tests:
    name: E2E Tests (Shard ${{ matrix.shard }})
    runs-on: ubuntu-latest
    needs: [unit-tests, api-tests]
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright
        run: npx playwright install --with-deps
      
      - name: Run E2E tests
        run: npx playwright test --shard=${{ matrix.shard }}/4
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.shard }}
          path: playwright-report/
          retention-days: 30
      
      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: screenshots-${{ matrix.shard }}
          path: screenshots/

  # Job 5: Generate Reports
  report:
    name: Generate Test Report
    runs-on: ubuntu-latest
    needs: e2e-tests
    if: always()
    steps:
      - uses: actions/checkout@v3
      
      - name: Download all artifacts
        uses: actions/download-artifact@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Generate dashboard
        run: node scripts/dashboard.js
      
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dashboard

  # Job 6: Notify
  notify:
    name: Send Notifications
    runs-on: ubuntu-latest
    needs: [e2e-tests]
    if: always()
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
      
      - name: Install dependencies
        run: npm ci
      
      - name: Send Slack notification
        run: node scripts/slack-notifier.js
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

  # Job 7: Deploy (only on main branch if all tests passed)
  deploy:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: [e2e-tests]
    if: github.ref == 'refs/heads/main' && success()
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to staging
        run: |
          echo "Deploying to staging..."
          # Your deployment script
      
      - name: Run smoke tests
        run: npm run test:smoke
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}

Complete Jenkinsfile

Jenkinsfile:

pipeline {
    agent any
    
    environment {
        NODE_VERSION = '18'
        STAGING_URL = credentials('staging-url')
        API_KEY = credentials('api-key')
        SLACK_WEBHOOK = credentials('slack-webhook')
    }
    
    options {
        timeout(time: 1, unit: 'HOURS')
        timestamps()
        buildDiscarder(logRotator(numToKeepStr: '30'))
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Setup') {
            steps {
                sh """
                    node --version
                    npm --version
                    npm ci
                """
            }
        }
        
        stage('Code Quality') {
            parallel {
                stage('Lint') {
                    steps {
                        sh 'npm run lint'
                    }
                }
                
                stage('Format Check') {
                    steps {
                        sh 'npm run format:check'
                    }
                }
            }
        }
        
        stage('Unit Tests') {
            steps {
                sh 'npm run test:unit'
            }
            post {
                always {
                    junit 'test-results/unit/*.xml'
                }
            }
        }
        
        stage('API Tests') {
            steps {
                sh 'npm run test:api'
            }
            post {
                always {
                    junit 'test-results/api/*.xml'
                }
            }
        }
        
        stage('E2E Tests') {
            steps {
                sh '''
                    npx playwright install --with-deps
                    npx playwright test
                '''
            }
            post {
                always {
                    publishHTML([
                        reportName: 'Playwright Report',
                        reportDir: 'playwright-report',
                        reportFiles: 'index.html',
                        keepAll: true
                    ])
                    
                    junit 'test-results/e2e/*.xml'
                    
                    archiveArtifacts artifacts: 'screenshots/**/*.png',
                                   allowEmptyArchive: true
                }
            }
        }
        
        stage('Generate Dashboard') {
            when {
                expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
            }
            steps {
                sh 'node scripts/dashboard.js'
                
                publishHTML([
                    reportName: 'Test Dashboard',
                    reportDir: 'dashboard',
                    reportFiles: 'index.html',
                    keepAll: true
                ])
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'main'
                expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
            }
            steps {
                sh './deploy-staging.sh'
            }
        }
        
        stage('Smoke Tests on Staging') {
            when {
                branch 'main'
            }
            steps {
                sh 'npm run test:smoke'
            }
        }
    }
    
    post {
        always {
            sh 'node scripts/slack-notifier.js'
        }
        
        failure {
            emailext(
                subject: "❌ Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: """
                    Build failed!
                    
                    Job: ${env.JOB_NAME}
                    Build: ${env.BUILD_NUMBER}
                    URL: ${env.BUILD_URL}
                    
                    Check the console output for details.
                """,
                to: 'qa-team@example.com'
            )
        }
        
        success {
            echo '✅ All stages completed successfully!'
        }
    }
}

🎓 Ресурсы для изучения

📺 YouTube Каналы

CI/CD:

  1. TechWorld with Nana

    • YouTube
    • ⭐⭐⭐⭐⭐ Лучший канал по DevOps
    • Jenkins, Docker, Kubernetes
  2. FreeCodeCamp - Jenkins Full Course

    • YouTube
    • 4+ часа бесплатного контента
  3. Automation Step by Step

Docker:

  1. Docker - Official Channel

  2. Fireship - Docker in 100 Seconds

    • YouTube
    • Быстрые, емкие видео

💻 Онлайн курсы

GitHub Actions:

  1. “GitHub Actions - The Complete Guide” - Academind

  2. GitHub Learning Lab (FREE!)

    • 🆓 Интерактивные туториалы
    • GitHub Skills

Jenkins:

  1. “Jenkins From Zero To Hero” - Udemy

    • 💰 $13.99
    • Complete Jenkins guide
    • 💰 Get Course
  2. “Learn DevOps: CI/CD with Jenkins Pipelines” - Udemy

Docker:

  1. “Docker Mastery” - Bret Fisher

    • 💰 Udemy: $12.99
    • 19+ часов
    • Industry standard
    • 💰 Get Course
  2. “Docker for QA Engineers” - Test Automation University

LinkedIn Learning:

  1. “Learning GitHub Actions”

  2. “Continuous Integration and Continuous Delivery (CI/CD)”


📚 Книги

1. “Continuous Delivery” - Jez Humble, David Farley

  • Библия CI/CD
  • Must-read для всех
  • 💰 Amazon - $45

2. “The Phoenix Project” - Gene Kim

  • DevOps роман
  • Понимание culture
  • 💰 Amazon - $18

3. “Docker Deep Dive” - Nigel Poulton


🏆 Практические ресурсы

1. Play with Docker

2. Katacoda/KillerCoda

  • 🆓 Интерактивные сценарии
  • Jenkins, Docker, Kubernetes
  • killercoda.com

3. GitHub Actions Marketplace


🎓 Сертификации

1. GitHub Actions Certification

2. Jenkins Engineer Certification

3. Docker Certified Associate


✅ Чек-лист: CI/CD Readiness

Git/GitHub:

  • Понимаю Git workflow
  • Умею работать с branches
  • Могу создать Pull Request
  • Знаю как делать code review
  • Понимаю Git conflicts resolution

GitHub Actions:

  • Написал базовый workflow
  • Настроил matrix strategy
  • Работал с secrets
  • Умею использовать artifacts
  • Настроил notifications

Jenkins:

  • Установил Jenkins локально
  • Написал Jenkinsfile
  • Настроил Jenkins pipeline
  • Знаком с Jenkins plugins
  • Настроил scheduled builds

Docker:

  • Написал Dockerfile
  • Создал docker-compose
  • Запускал тесты в контейнере
  • Понимаю Docker networking
  • Работал с Docker volumes

Reporting:

  • Настроил HTML reports
  • Интегрировал Allure
  • Создал custom dashboard
  • Настроил notifications

Общее:

  • Понимаю CI/CD principles
  • Могу объяснить test pyramid
  • Знаю best practices
  • Имею проект с CI/CD в GitHub

📍 Что дальше?

В следующей статье мы разберем Performance Testing:

  • JMeter для load testing
  • K6 для modern performance testing
  • Gatling advanced scenarios
  • Performance monitoring
  • Analyzing results

Следующая статья: Статья 6: Performance Testing


💡 Финальный совет для Apple Interview

Что ценит Apple в CI/CD:

  1. Automation First

    • Все должно быть автоматизировано
    • Минимум ручных шагов
  2. Fast Feedback

    • Тесты быстрые (< 10 мин)
    • Parallel execution
    • Smart test selection
  3. Reliability

    • Stable tests (no flaky tests)
    • Retry mechanisms
    • Clear failure reporting
  4. Observability

    • Good dashboards
    • Metrics tracking
    • Trend analysis

На интервью покажите:

  • ✅ GitHub repo с CI/CD setup
  • ✅ Jenkinsfile примеры
  • ✅ Docker containerized tests
  • ✅ Test reports/dashboards
  • ✅ Notifications setup

Будьте готовы объяснить:

  • Почему выбрали конкретный подход
  • Как решаете flaky tests
  • Как оптимизируете время выполнения
  • Как мониторите качество

Была ли статья полезна? 👏

Вопросы? Пишите в комментариях!


Автор: AAnnayev — Senior SDET Tags: #CICD #Jenkins #GitHubActions #Docker #DevOps #Apple #QA


P.S. Полный код проекта с CI/CD: github.com/yourname/cicd-test-framework

P.P.S. Только 2 статьи до финала! Статья 6 (Performance) и Статья 7 (Apple Job)! 🚀