ContinuousTesting continuous-testing

持续测试是一种软件开发实践,通过在软件开发生命周期中集成自动化测试,实现快速质量反馈,左移测试重点,并自动验证代码变更。关键词包括测试自动化、测试左移、测试金字塔、并行执行、测试门、不稳定测试、测试报告、测试选择策略。

测试 0 次安装 0 次浏览 更新于 3/3/2026

持续测试

概览

持续测试将自动化测试集成到软件开发生命周期中,每个阶段都能快速获得质量反馈。它将测试向左移动到开发过程中,并确保代码变更在到达生产环境之前自动验证。

何时使用

  • 建立CI/CD管道时
  • 提交时自动执行测试
  • 实施左移测试
  • 并行运行测试
  • 创建部署的测试门
  • 监控测试健康
  • 优化测试执行时间
  • 建立质量门

关键概念

  • 左移:在开发周期早期进行测试
  • 测试金字塔:单元 > 集成 > E2E测试
  • 并行执行:同时运行测试
  • 测试门:晋升前的质量要求
  • 不稳定测试:需要修复的不可靠测试
  • 测试报告:仪表板和指标
  • 测试选择:仅运行受影响的测试

指令

1. GitHub Actions CI管道

# .github/workflows/ci.yml
name: 持续测试

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

env:
  NODE_VERSION: '18'

jobs:
  # 单元测试 - 快速反馈
  unit-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v3

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: 安装依赖
        run: npm ci

      - name: 运行单元测试
        run: npm run test:unit -- --coverage

      - name: 上传覆盖率
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
          flags: unit

      - name: 在PR中评论覆盖率
        if: github.event_name == 'pull_request'
        uses: romeovs/lcov-reporter-action@v0.3.1
        with:
          lcov-file: ./coverage/lcov.info
          github-token: ${{ secrets.GITHUB_TOKEN }}

  # 集成测试
  integration-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v3

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: 安装依赖
        run: npm ci

      - name: 运行迁移
        run: npm run db:migrate
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

      - name: 运行集成测试
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
          REDIS_URL: redis://localhost:6379

  # E2E测试 - 并行运行
  e2e-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]

    steps:
      - uses: actions/checkout@v3

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: 安装Playwright浏览器
        run: npx playwright install --with-deps chromium

      - name: 构建应用
        run: npm run build

      - name: 运行E2E测试(分片 ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

      - name: 上传测试结果
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.shardIndex }}
          path: playwright-report/
          retention-days: 7

  # 视觉回归测试
  visual-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # 需要Percy

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: 安装依赖
        run: npm ci

      - name: 构建Storybook
        run: npm run build-storybook

      - name: 运行视觉测试
        run: npx percy storybook ./storybook-static
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

  # 安全扫描
  security-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v3

      - name: 运行npm审计
        run: npm audit --audit-level=high
        continue-on-error: true

      - name: 运行Snyk
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

      - name: 运行SAST与Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/owasp-top-ten

  # 性能测试
  performance-tests:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v3

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: 安装k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update
          sudo apt-get install k6

      - name: 运行性能测试
        run: k6 run tests/performance/load-test.js

  # 质量门 - 所有测试必须通过
  quality-gate:
    runs-on: ubuntu-latest
    needs:
      - unit-tests
      - integration-tests
      - e2e-tests
      - security-tests

    steps:
      - name: 检查测试结果
        run: echo "所有质量门已通过!"

      - name: 可以部署到暂存环境
        if: github.ref == 'refs/heads/develop'
        run: echo "准备部署到暂存环境"

      - name: 可以部署到生产环境
        if: github.ref == 'refs/heads/main'
        run: echo "准备部署到生产环境"

2. GitLab CI管道

# .gitlab-ci.yml
stages:
  - test
  - security
  - deploy

variables:
  POSTGRES_DB: test
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: postgres

# 测试模板
.test_template:
  image: node:18
  cache:
    paths:
      - node_modules/
  before_script:
    - npm ci

# 单元测试 - 每次提交都运行
unit-tests:
  extends: .test_template
  stage: test
  script:
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/

# 集成测试
integration-tests:
  extends: .test_template
  stage: test
  services:
    - postgres:14
    - redis:7
  variables:
    DATABASE_URL: postgresql://postgres:postgres@postgres:5432/test
  script:
    - npm run db:migrate
    - npm run test:integration

# E2E测试 - 并行执行
e2e-tests:
  extends: .test_template
  stage: test
  parallel: 4
  script:
    - npx playwright install --with-deps chromium
    - npm run build
    - npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
  artifacts:
    when: always
    paths:
      - playwright-report/
    expire_in: 7 days

# 安全扫描
security-scan:
  stage: security
  image: node:18
  script:
    - npm audit --audit-level=moderate
    - npx snyk test --severity-threshold=high
  allow_failure: true

# 契约测试
contract-tests:
  extends: .test_template
  stage: test
  script:
    - npm run test:pact
    - npx pact-broker publish ./pacts \
        --consumer-app-version=$CI_COMMIT_SHA \
        --broker-base-url=$PACT_BROKER_URL \
        --broker-token=$PACT_BROKER_TOKEN
  only:
    - merge_requests
    - main

3. Jenkins管道

// Jenkinsfile
pipeline {
    agent any

    environment {
        NODE_VERSION = '18'
        DATABASE_URL = credentials('test-database-url')
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Parallel Tests') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        sh 'npm run test:unit -- --coverage'
                    }
                    post {
                        always {
                            junit 'test-results/junit.xml'
                            publishCoverage adapters: [
                                coberturaAdapter('coverage/cobertura-coverage.xml')
                            ]
                        }
                    }
                }

                stage('Integration Tests') {
                    steps {
                        sh 'npm run test:integration'
                    }
                }

                stage('Lint & Type Check') {
                    steps {
                        sh 'npm run lint'
                        sh 'npm run type-check'
                    }
                }
            }
        }

        stage('E2E Tests') {
            steps {
                sh 'npx playwright install --with-deps'
                sh 'npm run build'
                sh 'npx playwright test'
            }
            post {
                always {
                    publishHTML([
                        reportDir: 'playwright-report',
                        reportFiles: 'index.html',
                        reportName: 'Playwright Report'
                    ])
                }
            }
        }

        stage('Security Scan') {
            steps {
                sh 'npm audit --audit-level=high'
                sh 'npx snyk test --severity-threshold=high'
            }
        }

        stage('Quality Gate') {
            steps {
                script {
                    def coverage = readFile('coverage/coverage-summary.json')
                    def coverageData = new groovy.json.JsonSlurper().parseText(coverage)
                    def lineCoverage = coverageData.total.lines.pct

                    if (lineCoverage < 80) {
                        error "Coverage ${lineCoverage}% is below threshold 80%"
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                echo 'Deploying to staging...'
                // 部署步骤
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input message: 'Deploy to production?', ok: 'Deploy'
                echo 'Deploying to production...'
                // 部署步骤
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        failure {
            slackSend(
                color: 'danger',
                message: "Build failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
            )
        }
        success {
            slackSend(
                color: 'good',
                message: "Build succeeded: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
            )
        }
    }
}

4. 测试选择策略

// scripts/run-affected-tests.ts
import { execSync } from 'child_process';
import * as fs from 'fs';

class AffectedTestRunner {
  getAffectedFiles(): string[] {
    // 从git获取变更文件
    const output = execSync('git diff --name-only HEAD~1', {
      encoding: 'utf-8',
    });
    return output.split('
').filter(Boolean);
  }

  getTestsForFiles(files: string[]): Set<string> {
    const tests = new Set<string>();

    for (const file of files) {
      if (file.endsWith('.test.ts') || file.endsWith('.spec.ts')) {
        // 文件已经是测试
        tests.add(file);
      } else if (file.endsWith('.ts')) {
        // 查找关联的测试文件
        const testFile = file.replace('.ts', '.test.ts');
        if (fs.existsSync(testFile)) {
          tests.add(testFile);
        }

        // 检查导入此文件的集成测试
        const integrationTests = execSync(
          `grep -r "from.*${file}" tests/integration/*.test.ts`,
          { encoding: 'utf-8' }
        ).split('
');

        integrationTests.forEach(line => {
          const match = line.match(/^([^:]+):/);
          if (match) tests.add(match[1]);
        });
      }
    }

    return tests;
  }

  run() {
    const affectedFiles = this.getAffectedFiles();
    console.log('Affected files:', affectedFiles);

    const testsToRun = this.getTestsForFiles(affectedFiles);
    console.log('Tests to run:', testsToRun);

    if (testsToRun.size === 0) {
      console.log('No tests affected');
      return;
    }

    // 仅运行受影响的测试
    const testPattern = Array.from(testsToRun).join('|');
    execSync(`npm test -- --testPathPattern="${testPattern}"`, {
      stdio: 'inherit',
    });
  }
}

new AffectedTestRunner().run();

5. 不稳定测试检测

# .github/workflows/flaky-test-detection.yml
name: 不稳定测试检测

on:
  schedule:
    - cron: '0 2 * * *'  # 每晚运行

jobs:
  detect-flaky-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: 安装依赖
        run: npm ci

      - name: 运行测试10次
        run: |
          for i in {1..10}; do
            echo "Run $i"
            npm test -- --json --outputFile=results-$i.json || true
          done

      - name: 分析不稳定测试
        run: node scripts/analyze-flaky-tests.js
// scripts/analyze-flaky-tests.js
const fs = require('fs');

const runs = Array.from({ length: 10 }, (_, i) =>
  JSON.parse(fs.readFileSync(`results-${i + 1}.json`, 'utf-8'))
);

const testResults = new Map();

// 聚合结果
runs.forEach(run => {
  run.testResults.forEach(suite => {
    suite.assertionResults.forEach(test => {
      const key = `${suite.name}::${test.title}`;
      if (!testResults.has(key)) {
        testResults.set(key, { passed: 0, failed: 0 });
      }
      const stats = testResults.get(key);
      if (test.status === 'passed') {
        stats.passed++;
      } else {
        stats.failed++;
      }
    });
  });
});

// 识别不稳定测试
const flakyTests = [];
testResults.forEach((stats, test) => {
  if (stats.passed > 0 && stats.failed > 0) {
    flakyTests.push({
      test,
      passRate: (stats.passed / 10) * 100,
      ...stats,
    });
  }
});

if (flakyTests.length > 0) {
  console.log('
不稳定测试检测到:');
  flakyTests.forEach(({ test, passRate, passed, failed }) => {
    console.log(`  ${test}`);
    console.log(`    通过率:${passRate}% (${passed}/10次运行)`);
  });

  // 如果不稳定测试太多,则失败
  if (flakyTests.length > 5) {
    process.exit(1);
  }
}

6. 测试指标仪表板

// scripts/generate-test-metrics.ts
import * as fs from 'fs';

interface TestMetrics {
  totalTests: number;
  passedTests: number;
  failedTests: number;
  skippedTests: number;
  duration: number;
  coverage: number;
  timestamp: string;
}

class MetricsCollector {
  collectMetrics(): TestMetrics {
    const testResults = JSON.parse(
      fs.readFileSync('test-results.json', 'utf-8')
    );
    const coverage = JSON.parse(
      fs.readFileSync('coverage/coverage-summary.json', 'utf-8')
    );

    return {
      totalTests: testResults.numTotalTests,
      passedTests: testResults.numPassedTests,
      failedTests: testResults.numFailedTests,
      skippedTests: testResults.numPendingTests,
      duration: testResults.testResults.reduce(
        (sum, r) => sum + r.perfStats.runtime,
        0
      ),
      coverage: coverage.total.lines.pct,
      timestamp: new Date().toISOString(),
    };
  }

  saveMetrics(metrics: TestMetrics) {
    const history = this.loadHistory();
    history.push(metrics);

    // 保留最后30天
    const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
    const filtered = history.filter(
      m => new Date(m.timestamp).getTime() > cutoff
    );

    fs.writeFileSync(
      'metrics-history.json',
      JSON.stringify(filtered, null, 2)
    );
  }

  loadHistory(): TestMetrics[] {
    try {
      return JSON.parse(fs.readFileSync('metrics-history.json', 'utf-8'));
    } catch {
      return [];
    }
  }

  generateReport() {
    const history = this.loadHistory();

    console.log('
测试指标(最近7天):');
    console.log('─'.repeat(60));

    const recent = history.slice(-7);
    const avgCoverage =
      recent.reduce((sum, m) => sum + m.coverage, 0) / recent.length;
    const avgDuration =
      recent.reduce((sum, m) => sum + m.duration, 0) / recent.length;

    console.log(`平均覆盖率:${avgCoverage.toFixed(2)}%`);
    console.log(`平均持续时间:${(avgDuration / 1000).toFixed(2)}s`);
    console.log(`总测试数:${recent[recent.length - 1].totalTests}`);
  }
}

const collector = new MetricsCollector();
const metrics = collector.collectMetrics();
collector.saveMetrics(metrics);
collector.generateReport();

最佳实践

✅ 执行

  • 先运行快速测试(单元 → 集成 → E2E)
  • 并行化测试执行
  • 缓存依赖
  • 设置适当的超时
  • 监控测试健康和不稳定性
  • 实施质量门
  • 使用测试选择策略
  • 生成全面的报告

❌ 不要

  • 顺序运行所有测试
  • 忽略不稳定测试
  • 跳过测试维护
  • 允许测试相互依赖
  • 每次提交都运行慢速测试
  • 有失败测试时部署
  • 忽略测试执行时间
  • 跳过安全扫描

跟踪指标

  • 测试覆盖率:行、分支、函数覆盖率
  • 测试持续时间:执行时间趋势
  • 通过率:通过测试的百分比
  • 不稳定性:结果不一致的测试
  • 测试计数:随时间增长
  • 构建成功率:管道可靠性

工具

  • CI/CD:GitHub Actions, GitLab CI, Jenkins, CircleCI
  • 测试编排:Nx, Bazel, Lerna
  • 报告:Allure, ReportPortal, TestRail
  • 监控:Datadog, New Relic, Grafana

示例

另见:test-automation-framework, integration-testing, cicd-pipeline-setup 了解全面的CI/CD实现。