Skip to content

测试自动化

📋 概述

测试自动化是将测试执行、结果分析、报告生成等过程自动化的实践。在Node.js项目中,测试自动化通过CI/CD流水线确保代码质量,提高开发效率,减少人工错误,是现代软件开发流程的重要组成部分。

🎯 学习目标

  • 理解测试自动化的核心概念和价值
  • 掌握CI/CD中的测试自动化配置
  • 学会构建完整的测试自动化流水线
  • 了解测试自动化的监控和优化策略

🔄 测试自动化策略

自动化测试金字塔

mermaid
graph TB
    A[测试自动化金字塔] --> B[单元测试<br/>Unit Tests]
    A --> C[集成测试<br/>Integration Tests]
    A --> D[E2E测试<br/>End-to-End Tests]
    A --> E[探索性测试<br/>Exploratory Tests]
    
    B --> B1[70% - 快速反馈<br/>开发者驱动<br/>高覆盖率]
    C --> C1[20% - 组件协作<br/>API验证<br/>数据流测试]
    D --> D2[9% - 用户场景<br/>关键路径<br/>业务验证]
    E --> E1[1% - 手工测试<br/>可用性测试<br/>创新场景]
    
    style B fill:#e8f5e8
    style C fill:#fff3e0
    style D fill:#ffebee
    style E fill:#f3e5f5

自动化测试策略

javascript
const TestAutomationStrategy = {
  LEVELS: {
    unit: {
      percentage: 70,
      characteristics: [
        '执行速度最快',
        '反馈最及时',
        '维护成本最低',
        '覆盖率最高'
      ],
      tools: ['Jest', 'Mocha', 'Vitest'],
      triggerPoints: [
        '代码提交时',
        '合并请求创建时',
        '本地开发时'
      ]
    },
    
    integration: {
      percentage: 20,
      characteristics: [
        '验证组件协作',
        '测试API接口',
        '数据库集成测试',
        '外部服务模拟'
      ],
      tools: ['Supertest', 'Testcontainers', 'Nock'],
      triggerPoints: [
        '功能分支合并前',
        '每日构建',
        '发布候选版本'
      ]
    },
    
    e2e: {
      percentage: 9,
      characteristics: [
        '模拟真实用户操作',
        '验证完整业务流程',
        '跨浏览器兼容性',
        '性能基准测试'
      ],
      tools: ['Playwright', 'Cypress', 'Puppeteer'],
      triggerPoints: [
        '发布前验证',
        '主分支更新后',
        '定时回归测试'
      ]
    },
    
    manual: {
      percentage: 1,
      characteristics: [
        '探索性测试',
        '可用性验证',
        '创新场景测试',
        '边界条件探索'
      ],
      focus: [
        '新功能验收',
        '用户体验评估',
        '安全性审查',
        '性能调优验证'
      ]
    }
  },
  
  AUTOMATION_PRINCIPLES: {
    failFast: '快速失败,快速反馈',
    parallelization: '并行执行,提高效率',
    reliability: '稳定可靠,减少误报',
    maintainability: '易于维护,持续改进',
    scalability: '可扩展性,支持团队成长'
  }
};

🏗️ CI/CD集成配置

GitHub Actions配置

yaml
# .github/workflows/test-automation.yml
name: Test Automation Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    # 每日凌晨2点执行完整测试
    - cron: '0 2 * * *'

env:
  NODE_VERSION: '18'
  CACHE_NAME: 'node-modules'

jobs:
  # 代码质量检查
  code-quality:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linting
        run: npm run lint
      
      - name: Run type checking
        run: npm run type-check
      
      - name: Check code formatting
        run: npm run format:check

  # 单元测试
  unit-tests:
    runs-on: ubuntu-latest
    needs: code-quality
    strategy:
      matrix:
        node-version: [16, 18, 20]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit
        env:
          CI: true
      
      - name: Generate coverage report
        run: npm run test:coverage
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella

  # 集成测试
  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Wait for PostgreSQL
        run: |
          until pg_isready -h localhost -p 5432; do
            echo "Waiting for PostgreSQL..."
            sleep 2
          done
      
      - name: Run database migrations
        run: npm run migrate:test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
      
      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
          REDIS_URL: redis://localhost:6379
          NODE_ENV: test

  # E2E测试
  e2e-tests:
    runs-on: ubuntu-latest
    needs: integration-tests
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}
      
      - name: Build application
        run: npm run build
      
      - name: Start application
        run: |
          npm run start:test &
          sleep 10
        env:
          NODE_ENV: test
          PORT: 3000
      
      - name: Run E2E tests
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          PLAYWRIGHT_BASE_URL: http://localhost:3000
      
      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/

  # 性能测试
  performance-tests:
    runs-on: ubuntu-latest
    needs: integration-tests
    if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build application
        run: npm run build
      
      - name: Start application
        run: |
          npm run start:prod &
          sleep 15
        env:
          NODE_ENV: production
          PORT: 3000
      
      - name: Run load tests
        run: npm run test:load
        env:
          TARGET_URL: http://localhost:3000
      
      - name: Performance baseline check
        run: npm run test:performance-baseline
      
      - name: Upload performance results
        uses: actions/upload-artifact@v3
        with:
          name: performance-results
          path: performance-results/

  # 安全测试
  security-tests:
    runs-on: ubuntu-latest
    needs: code-quality
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Run security audit
        run: npm audit --audit-level high
      
      - name: Run CodeQL analysis
        uses: github/codeql-action/analyze@v2
        with:
          languages: javascript
      
      - name: Run OWASP dependency check
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: 'nodejs-app'
          path: '.'
          format: 'HTML'
      
      - name: Upload security results
        uses: actions/upload-artifact@v3
        with:
          name: security-report
          path: reports/

  # 部署准备
  deployment-preparation:
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests, e2e-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build application
        run: npm run build
      
      - name: Run smoke tests
        run: npm run test:smoke
      
      - name: Create deployment package
        run: |
          tar -czf deployment.tar.gz \
            dist/ \
            package.json \
            package-lock.json \
            docker/ \
            scripts/
      
      - name: Upload deployment artifact
        uses: actions/upload-artifact@v3
        with:
          name: deployment-package
          path: deployment.tar.gz

GitLab CI配置

yaml
# .gitlab-ci.yml
stages:
  - validate
  - test
  - security
  - performance
  - deploy

variables:
  NODE_VERSION: "18"
  POSTGRES_DB: test_db
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: postgres
  POSTGRES_HOST_AUTH_METHOD: trust

# 缓存配置
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .npm/

# 代码质量检查
code-quality:
  stage: validate
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run lint
    - npm run type-check
    - npm run format:check
  artifacts:
    reports:
      junit: reports/lint-results.xml

# 单元测试
unit-tests:
  stage: test
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run test:unit -- --coverage --reporters=default --reporters=junit
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      junit: reports/unit-tests.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/

# 集成测试
integration-tests:
  stage: test
  image: node:${NODE_VERSION}
  services:
    - postgres:14
    - redis:7-alpine
  variables:
    DATABASE_URL: "postgres://postgres:postgres@postgres:5432/test_db"
    REDIS_URL: "redis://redis:6379"
  before_script:
    - npm ci --cache .npm --prefer-offline
    - npm run migrate:test
  script:
    - npm run test:integration
  artifacts:
    reports:
      junit: reports/integration-tests.xml

# E2E测试
e2e-tests:
  stage: test
  image: mcr.microsoft.com/playwright:v1.40.0-focal
  before_script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  script:
    - npm run start:test &
    - sleep 10
    - npx playwright test
  artifacts:
    when: failure
    paths:
      - playwright-report/
    expire_in: 30 days

# 安全扫描
security-scan:
  stage: security
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm audit --audit-level high
    - npx retire
  allow_failure: true
  artifacts:
    reports:
      junit: reports/security-scan.xml

# 性能测试
performance-tests:
  stage: performance
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  script:
    - npm run start:prod &
    - sleep 15
    - npm run test:load
    - npm run test:performance-baseline
  artifacts:
    paths:
      - performance-results/
  only:
    - main
    - schedules

# 部署到测试环境
deploy-staging:
  stage: deploy
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run build
    - npm run deploy:staging
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - main

# 部署到生产环境
deploy-production:
  stage: deploy
  image: node:${NODE_VERSION}
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run build
    - npm run deploy:production
  environment:
    name: production
    url: https://app.example.com
  when: manual
  only:
    - main

Jenkins Pipeline配置

groovy
// Jenkinsfile
pipeline {
    agent any
    
    environment {
        NODE_VERSION = '18'
        DATABASE_URL = 'postgres://postgres:password@localhost:5432/test_db'
        REDIS_URL = 'redis://localhost:6379'
    }
    
    options {
        timeout(time: 1, unit: 'HOURS')
        retry(3)
        skipStagesAfterUnstable()
    }
    
    stages {
        stage('Preparation') {
            steps {
                // 清理工作空间
                cleanWs()
                
                // 检出代码
                checkout scm
                
                // 设置Node.js环境
                script {
                    def nodeHome = tool name: "Node-${NODE_VERSION}", type: 'jenkins.plugins.nodejs.tools.NodeJSInstallation'
                    env.PATH = "${nodeHome}/bin:${env.PATH}"
                }
                
                // 安装依赖
                sh 'npm ci'
            }
        }
        
        stage('Code Quality') {
            parallel {
                stage('Linting') {
                    steps {
                        sh 'npm run lint'
                        publishHTML([
                            allowMissing: false,
                            alwaysLinkToLastBuild: true,
                            keepAll: true,
                            reportDir: 'reports',
                            reportFiles: 'eslint-report.html',
                            reportName: 'ESLint Report'
                        ])
                    }
                }
                
                stage('Type Checking') {
                    steps {
                        sh 'npm run type-check'
                    }
                }
                
                stage('Security Audit') {
                    steps {
                        sh 'npm audit --audit-level high'
                        
                        // OWASP依赖检查
                        script {
                            try {
                                sh 'npm run security:check'
                            } catch (Exception e) {
                                currentBuild.result = 'UNSTABLE'
                                echo "Security check failed: ${e.getMessage()}"
                            }
                        }
                    }
                }
            }
        }
        
        stage('Testing') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit -- --coverage --reporters=default --reporters=junit'
                        
                        // 发布测试结果
                        publishTestResults testResultsPattern: 'reports/unit-tests.xml'
                        
                        // 发布覆盖率报告
                        publishHTML([
                            allowMissing: false,
                            alwaysLinkToLastBuild: true,
                            keepAll: true,
                            reportDir: 'coverage/lcov-report',
                            reportFiles: 'index.html',
                            reportName: 'Coverage Report'
                        ])
                        
                        // 覆盖率阈值检查
                        script {
                            def coverage = readJSON file: 'coverage/coverage-summary.json'
                            def linesCoverage = coverage.total.lines.pct
                            
                            if (linesCoverage < 80) {
                                error "Coverage ${linesCoverage}% is below threshold of 80%"
                            }
                        }
                    }
                }
                
                stage('Integration Tests') {
                    steps {
                        // 启动测试服务
                        sh '''
                            docker-compose -f docker-compose.test.yml up -d postgres redis
                            sleep 10
                        '''
                        
                        // 运行数据库迁移
                        sh 'npm run migrate:test'
                        
                        // 运行集成测试
                        sh 'npm run test:integration'
                        
                        // 清理测试环境
                        sh 'docker-compose -f docker-compose.test.yml down'
                        
                        publishTestResults testResultsPattern: 'reports/integration-tests.xml'
                    }
                }
            }
        }
        
        stage('E2E Tests') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    changeRequest()
                }
            }
            steps {
                // 构建应用
                sh 'npm run build'
                
                // 启动应用
                sh '''
                    npm run start:test &
                    sleep 15
                '''
                
                // 运行E2E测试
                sh 'npx playwright test'
                
                // 发布E2E测试结果
                publishHTML([
                    allowMissing: false,
                    alwaysLinkToLastBuild: true,
                    keepAll: true,
                    reportDir: 'playwright-report',
                    reportFiles: 'index.html',
                    reportName: 'E2E Test Report'
                ])
            }
            post {
                failure {
                    archiveArtifacts artifacts: 'playwright-report/**/*', fingerprint: true
                }
            }
        }
        
        stage('Performance Tests') {
            when {
                branch 'main'
            }
            steps {
                // 启动生产版本
                sh '''
                    npm run start:prod &
                    sleep 20
                '''
                
                // 运行性能测试
                sh 'npm run test:load'
                sh 'npm run test:performance-baseline'
                
                // 发布性能报告
                publishHTML([
                    allowMissing: false,
                    alwaysLinkToLastBuild: true,
                    keepAll: true,
                    reportDir: 'performance-results',
                    reportFiles: 'index.html',
                    reportName: 'Performance Report'
                ])
            }
        }
        
        stage('Build & Package') {
            when {
                branch 'main'
            }
            steps {
                sh 'npm run build'
                
                // 创建部署包
                sh '''
                    tar -czf deployment.tar.gz \
                        dist/ \
                        package.json \
                        package-lock.json \
                        docker/ \
                        scripts/
                '''
                
                archiveArtifacts artifacts: 'deployment.tar.gz', fingerprint: true
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'main'
            }
            steps {
                // 部署到预发布环境
                sh 'npm run deploy:staging'
                
                // 运行冒烟测试
                sh 'npm run test:smoke -- --env=staging'
            }
        }
        
        stage('Deploy to Production') {
            when {
                allOf {
                    branch 'main'
                    // 手动触发生产部署
                    anyOf {
                        triggeredBy 'UserIdCause'
                        environment name: 'DEPLOY_TO_PROD', value: 'true'
                    }
                }
            }
            steps {
                // 生产部署确认
                input message: 'Deploy to production?', ok: 'Deploy',
                      submitterParameter: 'DEPLOYER'
                
                // 部署到生产环境
                sh 'npm run deploy:production'
                
                // 生产环境冒烟测试
                sh 'npm run test:smoke -- --env=production'
            }
        }
    }
    
    post {
        always {
            // 清理工作空间
            cleanWs()
        }
        
        success {
            // 成功通知
            script {
                if (env.BRANCH_NAME == 'main') {
                    slackSend(
                        channel: '#deployments',
                        color: 'good',
                        message: "✅ Pipeline succeeded for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
                    )
                }
            }
        }
        
        failure {
            // 失败通知
            slackSend(
                channel: '#build-failures',
                color: 'danger',
                message: "❌ Pipeline failed for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
            )
            
            // 发送邮件通知
            emailext(
                subject: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "Build failed. Check console output at ${env.BUILD_URL}",
                to: "${env.CHANGE_AUTHOR_EMAIL}, team@example.com"
            )
        }
        
        unstable {
            // 不稳定构建通知
            slackSend(
                channel: '#build-warnings',
                color: 'warning',
                message: "⚠️ Pipeline unstable for ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
            )
        }
    }
}

🔧 测试自动化工具集成

测试报告聚合

javascript
// scripts/test-report-aggregator.js
const fs = require('fs');
const path = require('path');
const xml2js = require('xml2js');

class TestReportAggregator {
  constructor() {
    this.reports = {
      unit: [],
      integration: [],
      e2e: [],
      performance: []
    };
    this.summary = {
      totalTests: 0,
      passedTests: 0,
      failedTests: 0,
      skippedTests: 0,
      coverage: null,
      duration: 0
    };
  }
  
  async aggregateReports() {
    console.log('🔄 Aggregating test reports...');
    
    // 聚合JUnit报告
    await this.aggregateJUnitReports();
    
    // 聚合覆盖率报告
    await this.aggregateCoverageReports();
    
    // 聚合性能报告
    await this.aggregatePerformanceReports();
    
    // 生成HTML报告
    await this.generateHTMLReport();
    
    // 生成JSON摘要
    await this.generateJSONSummary();
    
    console.log('✅ Test report aggregation completed');
  }
  
  async aggregateJUnitReports() {
    const reportPaths = [
      'reports/unit-tests.xml',
      'reports/integration-tests.xml',
      'reports/e2e-tests.xml'
    ];
    
    for (const reportPath of reportPaths) {
      if (fs.existsSync(reportPath)) {
        await this.parseJUnitReport(reportPath);
      }
    }
  }
  
  async parseJUnitReport(filePath) {
    const xmlContent = fs.readFileSync(filePath, 'utf8');
    const parser = new xml2js.Parser();
    
    try {
      const result = await parser.parseStringPromise(xmlContent);
      const testsuites = result.testsuites || result.testsuite;
      
      if (Array.isArray(testsuites)) {
        testsuites.forEach(suite => this.processSuite(suite));
      } else {
        this.processSuite(testsuites);
      }
    } catch (error) {
      console.warn(`Failed to parse JUnit report ${filePath}:`, error.message);
    }
  }
  
  processSuite(suite) {
    const tests = parseInt(suite.$.tests || 0);
    const failures = parseInt(suite.$.failures || 0);
    const errors = parseInt(suite.$.errors || 0);
    const skipped = parseInt(suite.$.skipped || 0);
    const time = parseFloat(suite.$.time || 0);
    
    this.summary.totalTests += tests;
    this.summary.failedTests += failures + errors;
    this.summary.skippedTests += skipped;
    this.summary.passedTests += tests - failures - errors - skipped;
    this.summary.duration += time;
  }
  
  async aggregateCoverageReports() {
    const coveragePath = 'coverage/coverage-summary.json';
    
    if (fs.existsSync(coveragePath)) {
      const coverageData = JSON.parse(fs.readFileSync(coveragePath, 'utf8'));
      this.summary.coverage = {
        lines: coverageData.total.lines.pct,
        functions: coverageData.total.functions.pct,
        branches: coverageData.total.branches.pct,
        statements: coverageData.total.statements.pct
      };
    }
  }
  
  async aggregatePerformanceReports() {
    const performancePath = 'performance-results/summary.json';
    
    if (fs.existsSync(performancePath)) {
      const performanceData = JSON.parse(fs.readFileSync(performancePath, 'utf8'));
      this.summary.performance = {
        averageResponseTime: performanceData.averageResponseTime,
        throughput: performanceData.throughput,
        errorRate: performanceData.errorRate
      };
    }
  }
  
  async generateHTMLReport() {
    const htmlTemplate = `
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>测试自动化报告</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 40px; }
            .header { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
            .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
            .card { background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; }
            .card h3 { margin-top: 0; color: #495057; }
            .metric { font-size: 2em; font-weight: bold; }
            .success { color: #28a745; }
            .danger { color: #dc3545; }
            .warning { color: #ffc107; }
            .info { color: #17a2b8; }
            .coverage-bar { background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden; }
            .coverage-fill { height: 100%; background: linear-gradient(90deg, #dc3545 0%, #ffc107 70%, #28a745 90%); }
        </style>
    </head>
    <body>
        <div class="header">
            <h1>📊 测试自动化报告</h1>
            <p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
        </div>
        
        <div class="summary">
            <div class="card">
                <h3>📋 测试概览</h3>
                <div class="metric success">${this.summary.passedTests}</div>
                <p>通过测试</p>
                <div class="metric danger">${this.summary.failedTests}</div>
                <p>失败测试</p>
                <div class="metric warning">${this.summary.skippedTests}</div>
                <p>跳过测试</p>
            </div>
            
            <div class="card">
                <h3>📈 覆盖率</h3>
                ${this.summary.coverage ? `
                <div style="margin-bottom: 10px;">
                    <span>行覆盖率: ${this.summary.coverage.lines}%</span>
                    <div class="coverage-bar">
                        <div class="coverage-fill" style="width: ${this.summary.coverage.lines}%"></div>
                    </div>
                </div>
                <div style="margin-bottom: 10px;">
                    <span>分支覆盖率: ${this.summary.coverage.branches}%</span>
                    <div class="coverage-bar">
                        <div class="coverage-fill" style="width: ${this.summary.coverage.branches}%"></div>
                    </div>
                </div>
                ` : '<p>无覆盖率数据</p>'}
            </div>
            
            <div class="card">
                <h3>⏱️ 执行时间</h3>
                <div class="metric info">${Math.round(this.summary.duration)}s</div>
                <p>总执行时间</p>
            </div>
            
            ${this.summary.performance ? `
            <div class="card">
                <h3>🚀 性能指标</h3>
                <p>平均响应时间: ${this.summary.performance.averageResponseTime}ms</p>
                <p>吞吐量: ${this.summary.performance.throughput} RPS</p>
                <p>错误率: ${this.summary.performance.errorRate}%</p>
            </div>
            ` : ''}
        </div>
        
        <div class="card">
            <h3>📊 测试结果详情</h3>
            <ul>
                <li>总测试数: ${this.summary.totalTests}</li>
                <li>通过率: ${((this.summary.passedTests / this.summary.totalTests) * 100).toFixed(1)}%</li>
                <li>失败率: ${((this.summary.failedTests / this.summary.totalTests) * 100).toFixed(1)}%</li>
            </ul>
        </div>
    </body>
    </html>
    `;
    
    fs.writeFileSync('reports/test-summary.html', htmlTemplate);
  }
  
  async generateJSONSummary() {
    const jsonSummary = {
      timestamp: new Date().toISOString(),
      summary: this.summary,
      status: this.summary.failedTests === 0 ? 'PASSED' : 'FAILED',
      recommendations: this.generateRecommendations()
    };
    
    fs.writeFileSync('reports/test-summary.json', JSON.stringify(jsonSummary, null, 2));
  }
  
  generateRecommendations() {
    const recommendations = [];
    
    if (this.summary.coverage && this.summary.coverage.lines < 80) {
      recommendations.push({
        type: 'COVERAGE',
        priority: 'HIGH',
        message: `代码覆盖率${this.summary.coverage.lines}%低于80%,建议增加测试用例`
      });
    }
    
    if (this.summary.failedTests > 0) {
      recommendations.push({
        type: 'FAILURES',
        priority: 'CRITICAL',
        message: `有${this.summary.failedTests}个测试失败,需要立即修复`
      });
    }
    
    if (this.summary.duration > 600) { // 10分钟
      recommendations.push({
        type: 'PERFORMANCE',
        priority: 'MEDIUM',
        message: '测试执行时间过长,建议优化测试性能或增加并行度'
      });
    }
    
    return recommendations;
  }
}

// 使用示例
async function main() {
  const aggregator = new TestReportAggregator();
  await aggregator.aggregateReports();
}

if (require.main === module) {
  main().catch(console.error);
}

module.exports = TestReportAggregator;

测试环境管理

javascript
// scripts/test-environment-manager.js
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

class TestEnvironmentManager {
  constructor() {
    this.services = new Map();
    this.environments = {
      unit: {
        services: [],
        env: { NODE_ENV: 'test' }
      },
      integration: {
        services: ['postgres', 'redis'],
        env: {
          NODE_ENV: 'test',
          DATABASE_URL: 'postgres://postgres:password@localhost:5432/test_db',
          REDIS_URL: 'redis://localhost:6379'
        }
      },
      e2e: {
        services: ['postgres', 'redis', 'app'],
        env: {
          NODE_ENV: 'test',
          PORT: '3001',
          DATABASE_URL: 'postgres://postgres:password@localhost:5432/e2e_test_db',
          REDIS_URL: 'redis://localhost:6379'
        }
      }
    };
  }
  
  async setupEnvironment(type) {
    console.log(`🚀 Setting up ${type} test environment...`);
    
    const env = this.environments[type];
    if (!env) {
      throw new Error(`Unknown environment type: ${type}`);
    }
    
    // 设置环境变量
    Object.assign(process.env, env.env);
    
    // 启动所需服务
    for (const service of env.services) {
      await this.startService(service);
    }
    
    // 等待服务就绪
    await this.waitForServices(env.services);
    
    // 运行初始化脚本
    await this.runInitializationScripts(type);
    
    console.log(`✅ ${type} test environment ready`);
  }
  
  async startService(serviceName) {
    if (this.services.has(serviceName)) {
      console.log(`Service ${serviceName} already running`);
      return;
    }
    
    console.log(`Starting service: ${serviceName}`);
    
    switch (serviceName) {
      case 'postgres':
        await this.startPostgreSQL();
        break;
      case 'redis':
        await this.startRedis();
        break;
      case 'app':
        await this.startApplication();
        break;
      default:
        throw new Error(`Unknown service: ${serviceName}`);
    }
  }
  
  async startPostgreSQL() {
    const dockerCmd = [
      'docker', 'run', '-d',
      '--name', 'test-postgres',
      '-p', '5432:5432',
      '-e', 'POSTGRES_PASSWORD=password',
      '-e', 'POSTGRES_DB=test_db',
      'postgres:14'
    ];
    
    try {
      await this.execCommand(dockerCmd);
      this.services.set('postgres', { type: 'docker', container: 'test-postgres' });
    } catch (error) {
      // 可能容器已存在,尝试启动
      await this.execCommand(['docker', 'start', 'test-postgres']);
      this.services.set('postgres', { type: 'docker', container: 'test-postgres' });
    }
  }
  
  async startRedis() {
    const dockerCmd = [
      'docker', 'run', '-d',
      '--name', 'test-redis',
      '-p', '6379:6379',
      'redis:7-alpine'
    ];
    
    try {
      await this.execCommand(dockerCmd);
      this.services.set('redis', { type: 'docker', container: 'test-redis' });
    } catch (error) {
      await this.execCommand(['docker', 'start', 'test-redis']);
      this.services.set('redis', { type: 'docker', container: 'test-redis' });
    }
  }
  
  async startApplication() {
    const appProcess = spawn('npm', ['run', 'start:test'], {
      env: process.env,
      stdio: 'pipe'
    });
    
    this.services.set('app', { type: 'process', process: appProcess });
    
    // 监听应用输出
    appProcess.stdout.on('data', (data) => {
      console.log(`[APP] ${data}`);
    });
    
    appProcess.stderr.on('data', (data) => {
      console.error(`[APP ERROR] ${data}`);
    });
  }
  
  async waitForServices(services) {
    const checks = services.map(service => this.waitForService(service));
    await Promise.all(checks);
  }
  
  async waitForService(serviceName) {
    console.log(`Waiting for ${serviceName} to be ready...`);
    
    const maxAttempts = 30;
    const delay = 1000;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const isReady = await this.checkServiceHealth(serviceName);
        if (isReady) {
          console.log(`✅ ${serviceName} is ready`);
          return;
        }
      } catch (error) {
        console.log(`Attempt ${attempt}/${maxAttempts}: ${serviceName} not ready yet`);
      }
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    throw new Error(`${serviceName} failed to start within ${maxAttempts} attempts`);
  }
  
  async checkServiceHealth(serviceName) {
    switch (serviceName) {
      case 'postgres':
        return await this.checkPostgresHealth();
      case 'redis':
        return await this.checkRedisHealth();
      case 'app':
        return await this.checkAppHealth();
      default:
        return true;
    }
  }
  
  async checkPostgresHealth() {
    try {
      await this.execCommand([
        'docker', 'exec', 'test-postgres',
        'pg_isready', '-U', 'postgres'
      ]);
      return true;
    } catch {
      return false;
    }
  }
  
  async checkRedisHealth() {
    try {
      await this.execCommand([
        'docker', 'exec', 'test-redis',
        'redis-cli', 'ping'
      ]);
      return true;
    } catch {
      return false;
    }
  }
  
  async checkAppHealth() {
    try {
      const http = require('http');
      const port = process.env.PORT || 3001;
      
      return new Promise((resolve) => {
        const req = http.get(`http://localhost:${port}/health`, (res) => {
          resolve(res.statusCode === 200);
        });
        
        req.on('error', () => resolve(false));
        req.setTimeout(1000, () => {
          req.destroy();
          resolve(false);
        });
      });
    } catch {
      return false;
    }
  }
  
  async runInitializationScripts(type) {
    const scriptsDir = path.join(process.cwd(), 'scripts', 'test-init');
    const scriptFile = path.join(scriptsDir, `${type}.js`);
    
    if (fs.existsSync(scriptFile)) {
      console.log(`Running initialization script for ${type}...`);
      const initScript = require(scriptFile);
      await initScript();
    }
  }
  
  async teardownEnvironment() {
    console.log('🧹 Tearing down test environment...');
    
    for (const [serviceName, service] of this.services) {
      try {
        if (service.type === 'docker') {
          await this.execCommand(['docker', 'stop', service.container]);
          await this.execCommand(['docker', 'rm', service.container]);
        } else if (service.type === 'process') {
          service.process.kill('SIGTERM');
        }
        console.log(`Stopped service: ${serviceName}`);
      } catch (error) {
        console.warn(`Failed to stop service ${serviceName}:`, error.message);
      }
    }
    
    this.services.clear();
    console.log('✅ Test environment cleaned up');
  }
  
  async execCommand(command) {
    return new Promise((resolve, reject) => {
      const process = spawn(command[0], command.slice(1), { stdio: 'pipe' });
      
      let stdout = '';
      let stderr = '';
      
      process.stdout.on('data', (data) => stdout += data);
      process.stderr.on('data', (data) => stderr += data);
      
      process.on('close', (code) => {
        if (code === 0) {
          resolve(stdout);
        } else {
          reject(new Error(`Command failed: ${command.join(' ')}\\n${stderr}`));
        }
      });
    });
  }
}

module.exports = TestEnvironmentManager;

📝 测试自动化最佳实践

监控和优化

javascript
const TestAutomationBestPractices = {
  PIPELINE_OPTIMIZATION: {
    parallelization: [
      '并行执行独立的测试套件',
      '使用测试分片减少执行时间',
      '合理配置CI/CD工作器数量',
      '优化依赖安装和缓存策略'
    ],
    
    failFast: [
      '快速失败原则,及时终止失败的构建',
      '优先执行快速测试',
      '设置合理的超时时间',
      '智能重试机制'
    ],
    
    caching: [
      '缓存依赖包安装',
      '缓存构建产物',
      '缓存测试环境镜像',
      '增量测试执行'
    ]
  },
  
  QUALITY_GATES: {
    coverage: [
      '设置代码覆盖率阈值',
      '强制覆盖率不能下降',
      '分类别设置不同的覆盖率要求',
      '覆盖率趋势分析'
    ],
    
    performance: [
      '建立性能基准线',
      '监控性能回归',
      '设置响应时间阈值',
      '资源使用限制'
    ],
    
    security: [
      '依赖漏洞扫描',
      '代码安全审计',
      '敏感信息检测',
      '许可证合规检查'
    ]
  },
  
  MONITORING: {
    metrics: [
      '测试执行时间趋势',
      '测试成功率统计',
      '环境稳定性指标',
      '资源使用情况'
    ],
    
    alerting: [
      '测试失败即时通知',
      '覆盖率下降警告',
      '性能回归提醒',
      '环境异常报警'
    ]
  }
};

📝 总结

测试自动化是现代软件开发的核心实践:

  • 全流程覆盖:从代码提交到生产部署的完整自动化
  • 质量保证:多层次测试确保代码质量和系统稳定性
  • 效率提升:自动化执行减少人工成本和错误
  • 持续改进:监控和优化测试流程,不断提升效果

通过系统化的测试自动化实施,可以显著提高开发效率和产品质量。

🔗 相关资源