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.jsJenkins 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_SUITE2. 测试结果缓存
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故障排除
常见问题
超时问题
yamltimeout: 30m retry: max_attempts: 2 when: - runner_system_failure - stuck_or_timeout_failure资源限制
yamlresources: requests: memory: "2Gi" cpu: "1" limits: memory: "4Gi" cpu: "2"并发冲突
groovyoptions { lock resource: 'shared-test-db' }
总结
成功的 CI/CD 测试集成需要:
- 🚀 快速反馈: 及早发现问题
- 🔄 自动化: 减少手动干预
- 📊 可视化: 清晰的测试报告
- 🛡️ 质量门控: 确保代码质量
- 🔧 持续优化: 提高流水线效率
通过完善的 CI/CD 测试集成,确保每次代码变更的质量。