Skip to content

CI/CD 集成

将测试无缝集成到持续集成和持续部署流程中

CI/CD 测试策略

GitHub Actions

完整的测试工作流

yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 代码质量检查
  code-quality:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # 获取完整历史用于 SonarQube
      
      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      
      - name: ESLint
        run: |
          npm ci
          npm run lint -- --format json --output-file eslint-report.json
      
      - name: Upload ESLint Report
        uses: actions/upload-artifact@v3
        with:
          name: eslint-report
          path: eslint-report.json

  # 单元测试
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]
    steps:
      - uses: actions/checkout@v3
      
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: |
          npm run test:unit -- --coverage --ci --reporters=default --reporters=jest-junit
        env:
          JEST_JUNIT_OUTPUT_DIR: ./reports/junit
          JEST_JUNIT_OUTPUT_NAME: unit-tests.xml
      
      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: unit-test-results-${{ matrix.node-version }}
          path: |
            coverage/
            reports/junit/
      
      - name: Code Coverage Report
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          flags: unittests
          name: codecov-umbrella

  # 集成测试
  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: unit-tests
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        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: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
      
      - name: Upload test results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: integration-test-results
          path: test-results/

  # 构建和推送镜像
  build-and-push:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    needs: [code-quality, unit-tests, integration-tests]
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Log in to Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix={{branch}}-
      
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            BUILD_DATE=${{ github.event.head_commit.timestamp }}
            VCS_REF=${{ github.sha }}

  # 部署到测试环境
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build-and-push
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Kubernetes
        uses: azure/k8s-deploy@v4
        with:
          namespace: staging
          manifests: |
            k8s/staging/
          images: |
            ${{ needs.build-and-push.outputs.image-tag }}

  # API 测试
  api-tests:
    name: API Tests
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Postman Collection
        uses: matt-ball/newman-action@master
        with:
          collection: tests/api/postman_collection.json
          environment: tests/api/staging_environment.json
          reporters: cli,junit,htmlextra
          reporter-junit-export: newman-report.xml
          reporter-htmlextra-export: newman-report.html
      
      - name: Upload API test report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: api-test-report
          path: |
            newman-report.xml
            newman-report.html

  # 性能测试
  performance-tests:
    name: Performance Tests
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v3
      
      - name: Run K6 performance tests
        uses: grafana/k6-action@v0.3.0
        with:
          filename: tests/performance/load-test.js
          flags: --out json=performance-results.json
        env:
          K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
      
      - name: Upload performance results
        uses: actions/upload-artifact@v3
        with:
          name: performance-results
          path: performance-results.json

  # 安全扫描
  security-scan:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: deploy-staging
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ needs.build-and-push.outputs.image-tag }}
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'
      
      - name: OWASP ZAP Scan
        uses: zaproxy/action-full-scan@v0.4.0
        with:
          target: 'https://staging.example.com'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'

  # 部署到生产
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: [api-tests, performance-tests, security-scan]
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://api.example.com
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Production
        run: |
          echo "Deploying to production..."
          # 实际部署命令

GitLab CI/CD

完整的 GitLab Pipeline

yaml
# .gitlab-ci.yml
stages:
  - build
  - test
  - package
  - deploy
  - verify

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  FF_USE_FASTZIP: "true"
  ARTIFACT_COMPRESSION_LEVEL: "fast"
  CACHE_COMPRESSION_LEVEL: "fast"

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

# 构建阶段
build:frontend:
  stage: build
  image: node:18-alpine
  <<: *cache_config
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

build:backend:
  stage: build
  image: maven:3.8-openjdk-17
  <<: *cache_config
  script:
    - mvn clean compile -DskipTests
  artifacts:
    paths:
      - target/
    expire_in: 1 week

# 测试阶段
test:unit:
  stage: test
  image: node:18-alpine
  needs: ["build:frontend"]
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run test:unit -- --coverage
  artifacts:
    when: always
    reports:
      junit: coverage/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/

test:integration:
  stage: test
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
    - postgres:14
    - redis:7
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    REDIS_HOST: redis
  needs: ["build:backend"]
  script:
    - apk add --no-cache docker-compose
    - docker-compose -f docker-compose.test.yml up --abort-on-container-exit
  artifacts:
    when: always
    reports:
      junit: test-results/junit.xml

test:e2e:
  stage: test
  image: mcr.microsoft.com/playwright:v1.40.0-focal
  needs: ["build:frontend", "build:backend"]
  services:
    - name: postgres:14
      alias: db
    - name: redis:7
      alias: cache
  script:
    - npm ci
    - npm run test:e2e
  artifacts:
    when: always
    paths:
      - test-results/
      - playwright-report/
    reports:
      junit: test-results/junit.xml

# 安全扫描
security:sast:
  stage: test
  needs: []
  include:
    - template: Security/SAST.gitlab-ci.yml

security:dependency_scanning:
  stage: test
  needs: []
  include:
    - template: Security/Dependency-Scanning.gitlab-ci.yml

security:container_scanning:
  stage: test
  needs: ["package:docker"]
  include:
    - template: Security/Container-Scanning.gitlab-ci.yml

# 打包阶段
package:docker:
  stage: package
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  needs: ["test:unit", "test:integration"]
  before_script:
    - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
  script:
    - docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} .
    - docker tag ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} ${CI_REGISTRY_IMAGE}:latest
    - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
    - docker push ${CI_REGISTRY_IMAGE}:latest

# 部署阶段
deploy:staging:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: ["package:docker"]
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - develop
  script:
    - kubectl set image deployment/api api=${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} -n staging
    - kubectl rollout status deployment/api -n staging

deploy:production:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: ["package:docker", "verify:staging"]
  environment:
    name: production
    url: https://api.example.com
  only:
    - main
  when: manual
  script:
    - kubectl set image deployment/api api=${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA} -n production
    - kubectl rollout status deployment/api -n production

# 验证阶段
verify:staging:
  stage: verify
  image: postman/newman:alpine
  needs: ["deploy:staging"]
  script:
    - newman run tests/api/collection.json -e tests/api/staging.json --reporters cli,junit
  artifacts:
    reports:
      junit: newman-report.xml

verify:production:
  stage: verify
  image: grafana/k6:latest
  needs: ["deploy:production"]
  script:
    - k6 run tests/performance/smoke-test.js

Jenkins Pipeline

声明式 Pipeline

groovy
// Jenkinsfile
pipeline {
    agent {
        kubernetes {
            yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: node
    image: node:18
    command: ['cat']
    tty: true
  - name: docker
    image: docker:20.10
    command: ['cat']
    tty: true
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
  volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
"""
        }
    }
    
    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        DOCKER_CREDENTIALS = credentials('docker-registry')
        SONAR_TOKEN = credentials('sonar-token')
        SLACK_WEBHOOK = credentials('slack-webhook')
    }
    
    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 1, unit: 'HOURS')
        parallelsAlwaysFailFast()
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        script: "git rev-parse --short HEAD",
                        returnStdout: true
                    ).trim()
                }
            }
        }
        
        stage('Quality Gates') {
            parallel {
                stage('Lint') {
                    steps {
                        container('node') {
                            sh 'npm ci'
                            sh 'npm run lint'
                        }
                    }
                }
                
                stage('Security Scan') {
                    steps {
                        container('node') {
                            sh 'npm audit --production'
                            sh 'npm run security:check'
                        }
                    }
                }
                
                stage('SonarQube') {
                    steps {
                        container('node') {
                            withSonarQubeEnv('SonarQube') {
                                sh """
                                    npm run test:coverage
                                    sonar-scanner \
                                        -Dsonar.projectKey=${env.JOB_NAME} \
                                        -Dsonar.sources=src \
                                        -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
                                """
                            }
                        }
                    }
                }
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps {
                        container('node') {
                            sh 'npm run test:unit -- --ci'
                        }
                    }
                    post {
                        always {
                            junit 'test-results/unit/*.xml'
                            publishHTML([
                                allowMissing: false,
                                alwaysLinkToLastBuild: true,
                                keepAll: true,
                                reportDir: 'coverage',
                                reportFiles: 'index.html',
                                reportName: 'Coverage Report'
                            ])
                        }
                    }
                }
                
                stage('Integration Tests') {
                    steps {
                        script {
                            docker.image('postgres:14').withRun('-e POSTGRES_PASSWORD=test') { db ->
                                docker.image('redis:7').withRun() { cache ->
                                    container('node') {
                                        sh """
                                            export DATABASE_URL=postgresql://postgres:test@${db.id}:5432/test
                                            export REDIS_URL=redis://${cache.id}:6379
                                            npm run test:integration
                                        """
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        
        stage('Build') {
            steps {
                container('docker') {
                    sh """
                        docker build -t ${DOCKER_REGISTRY}/api:${GIT_COMMIT_SHORT} .
                        docker tag ${DOCKER_REGISTRY}/api:${GIT_COMMIT_SHORT} ${DOCKER_REGISTRY}/api:latest
                    """
                }
            }
        }
        
        stage('Push') {
            when {
                branch 'main'
            }
            steps {
                container('docker') {
                    sh """
                        echo ${DOCKER_CREDENTIALS_PSW} | docker login ${DOCKER_REGISTRY} -u ${DOCKER_CREDENTIALS_USR} --password-stdin
                        docker push ${DOCKER_REGISTRY}/api:${GIT_COMMIT_SHORT}
                        docker push ${DOCKER_REGISTRY}/api:latest
                    """
                }
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            stages {
                stage('Deploy to Staging') {
                    steps {
                        script {
                            deployToKubernetes('staging', env.GIT_COMMIT_SHORT)
                        }
                    }
                }
                
                stage('Smoke Tests') {
                    steps {
                        sh 'npm run test:smoke -- --env=staging'
                    }
                }
                
                stage('Deploy to Production') {
                    input {
                        message "Deploy to production?"
                        ok "Deploy"
                        parameters {
                            choice(
                                name: 'DEPLOYMENT_TYPE',
                                choices: ['Rolling Update', 'Blue/Green', 'Canary'],
                                description: 'Select deployment strategy'
                            )
                        }
                    }
                    steps {
                        script {
                            deployToKubernetes('production', env.GIT_COMMIT_SHORT, params.DEPLOYMENT_TYPE)
                        }
                    }
                }
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        success {
            slackSend(
                channel: '#ci-cd',
                color: 'good',
                message: "✅ Build Successful: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
        failure {
            slackSend(
                channel: '#ci-cd',
                color: 'danger',
                message: "❌ Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
    }
}

def deployToKubernetes(environment, version, strategy = 'Rolling Update') {
    sh """
        kubectl set image deployment/api api=${DOCKER_REGISTRY}/api:${version} -n ${environment}
        kubectl rollout status deployment/api -n ${environment}
    """
}

CircleCI

高级配置示例

yaml
# .circleci/config.yml
version: 2.1

orbs:
  node: circleci/node@5.0
  docker: circleci/docker@2.1
  kubernetes: circleci/kubernetes@1.3
  slack: circleci/slack@4.10

executors:
  node-executor:
    docker:
      - image: cimg/node:18.0
    working_directory: ~/repo
  
  integration-executor:
    docker:
      - image: cimg/node:18.0
      - image: cimg/postgres:14.0
        environment:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
      - image: cimg/redis:7.0

commands:
  restore_npm_cache:
    steps:
      - restore_cache:
          keys:
            - v1-npm-{{ checksum "package-lock.json" }}
            - v1-npm-

  save_npm_cache:
    steps:
      - save_cache:
          key: v1-npm-{{ checksum "package-lock.json" }}
          paths:
            - ~/.npm

jobs:
  lint-and-test:
    executor: node-executor
    steps:
      - checkout
      - restore_npm_cache
      - run:
          name: Install dependencies
          command: npm ci
      - save_npm_cache
      - run:
          name: Lint
          command: npm run lint
      - run:
          name: Unit Tests
          command: |
            npm run test:unit -- --coverage --ci --reporters=default --reporters=jest-junit
          environment:
            JEST_JUNIT_OUTPUT_DIR: ./reports/junit/
      - store_test_results:
          path: ./reports/junit/
      - store_artifacts:
          path: ./coverage
      - persist_to_workspace:
          root: .
          paths:
            - node_modules
            - coverage

  integration-tests:
    executor: integration-executor
    steps:
      - checkout
      - attach_workspace:
          at: .
      - run:
          name: Wait for services
          command: |
            dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
      - run:
          name: Integration Tests
          command: npm run test:integration
          environment:
            DATABASE_URL: postgresql://test:test@localhost:5432/testdb
            REDIS_URL: redis://localhost:6379
      - store_test_results:
          path: ./test-results

  build-and-push:
    executor: docker/docker
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      - docker/check
      - docker/build:
          image: myapp
          tag: ${CIRCLE_SHA1:0:7},latest
      - docker/push:
          image: myapp
          tag: ${CIRCLE_SHA1:0:7},latest

  deploy:
    executor: kubernetes/default
    parameters:
      environment:
        type: string
    steps:
      - checkout
      - kubernetes/install-kubectl
      - kubernetes/update-container-image:
          container-image-updates: api=myapp:${CIRCLE_SHA1:0:7}
          namespace: << parameters.environment >>
          resource-name: deployment/api

  performance-test:
    docker:
      - image: grafana/k6:latest
    steps:
      - checkout
      - run:
          name: Run performance tests
          command: |
            k6 run tests/performance/load-test.js \
              --out json=performance-results.json \
              --summary-export=summary.json
      - store_artifacts:
          path: performance-results.json
      - store_artifacts:
          path: summary.json

workflows:
  version: 2
  build-test-deploy:
    jobs:
      - lint-and-test
      - integration-tests:
          requires:
            - lint-and-test
      - build-and-push:
          requires:
            - integration-tests
          filters:
            branches:
              only:
                - main
                - develop
      - deploy:
          name: deploy-staging
          environment: staging
          requires:
            - build-and-push
          filters:
            branches:
              only: develop
      - performance-test:
          requires:
            - deploy-staging
      - hold:
          type: approval
          requires:
            - deploy-staging
          filters:
            branches:
              only: main
      - deploy:
          name: deploy-production
          environment: production
          requires:
            - hold
          filters:
            branches:
              only: main
      - slack/notify:
          event: fail
          template: basic_fail_1
          requires:
            - deploy-production
      - slack/notify:
          event: pass
          template: success_tagged_deploy_1
          requires:
            - deploy-production

  nightly:
    triggers:
      - schedule:
          cron: "0 0 * * *"
          filters:
            branches:
              only:
                - main
    jobs:
      - performance-test

测试报告集成

Allure 报告

groovy
// Jenkins 集成 Allure
stage('Generate Allure Report') {
    steps {
        script {
            allure([
                includeProperties: false,
                jdk: '',
                properties: [],
                reportBuildPolicy: 'ALWAYS',
                results: [[path: 'allure-results']]
            ])
        }
    }
}

测试趋势分析

yaml
# GitHub Actions 测试趋势
- name: Test Report
  uses: dorny/test-reporter@v1
  if: success() || failure()
  with:
    name: Test Results
    path: 'test-results/*.xml'
    reporter: java-junit
    fail-on-error: true

- name: Upload test trends
  uses: actions/upload-artifact@v3
  with:
    name: test-trends
    path: |
      .test-trends/
      coverage-history/

最佳实践

1. 并行化测试执行

yaml
test:
  parallel:
    matrix:
      - TEST_SUITE: [unit, integration, e2e]
  script:
    - npm run test:$TEST_SUITE

2. 测试结果缓存

groovy
cache(maxCacheSize: 250, caches: [
    arbitraryFileCache(
        path: 'node_modules',
        includes: '**/*',
        fingerprinting: true
    )
]) {
    // 测试执行
}

3. 失败快速反馈

yaml
fail_fast:
  stop_on_first_failure: true
  
notifications:
  slack:
    on_failure: always
    on_success: change

故障排除

常见问题

  1. 超时问题

    yaml
    timeout: 30m
    retry:
      max_attempts: 2
      when:
        - runner_system_failure
        - stuck_or_timeout_failure
  2. 资源限制

    yaml
    resources:
      requests:
        memory: "2Gi"
        cpu: "1"
      limits:
        memory: "4Gi"
        cpu: "2"
  3. 并发冲突

    groovy
    options {
        lock resource: 'shared-test-db'
    }

总结

成功的 CI/CD 测试集成需要:

  • 🚀 快速反馈: 及早发现问题
  • 🔄 自动化: 减少手动干预
  • 📊 可视化: 清晰的测试报告
  • 🛡️ 质量门控: 确保代码质量
  • 🔧 持续优化: 提高流水线效率

通过完善的 CI/CD 测试集成,确保每次代码变更的质量。

SOLO Development Guide