本章概述

Helm 提供了强大的测试框架,帮助我们验证 Chart 的正确性和应用的健康状态。通过合理的测试策略,我们可以确保 Chart 在各种环境中都能正常工作,提高部署的可靠性和稳定性。本章将深入探讨 Helm 测试的各种方法和最佳实践。

学习目标

  • 理解 Helm 测试框架的概念和架构
  • 掌握 Chart 测试的编写方法
  • 学会使用 helm test 命令进行测试
  • 了解不同类型的测试策略
  • 掌握测试的调试和故障排除
  • 学习 CI/CD 中的 Chart 测试集成
  • 了解测试的最佳实践和规范

6.1 Helm 测试框架概述

6.1.1 测试框架架构

graph TB
    A[helm test] --> B[Test Hooks]
    B --> C[Test Pods]
    C --> D[Test Execution]
    D --> E[Result Collection]
    E --> F[Test Report]
    
    subgraph "测试类型"
        G[Unit Tests]
        H[Integration Tests]
        I[End-to-End Tests]
        J[Health Checks]
        K[Performance Tests]
    end
    
    subgraph "测试阶段"
        L[Pre-deployment]
        M[Post-deployment]
        N[Runtime]
        O[Pre-upgrade]
        P[Post-upgrade]
    end

6.1.2 测试的优势

  1. 质量保证:确保 Chart 的正确性和稳定性
  2. 自动化验证:自动验证应用的功能和性能
  3. 回归测试:防止新版本引入问题
  4. 环境验证:验证不同环境下的兼容性
  5. 持续集成:集成到 CI/CD 流程中

6.2 基础测试编写

6.2.1 简单的健康检查测试

# templates/tests/health-check.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-health-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: health-check
    image: curlimages/curl:7.85.0
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting health check test..."
      
      # 基本健康检查
      echo "Testing health endpoint..."
      curl -f http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health
      
      if [ $? -eq 0 ]; then
        echo "✓ Health check passed"
      else
        echo "✗ Health check failed"
        exit 1
      fi
      
      echo "Health check test completed successfully"

6.2.2 API 功能测试

# templates/tests/api-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-api-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: api-test
    image: curlimages/curl:7.85.0
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting API functionality test..."
      
      BASE_URL="http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
      
      # 测试 API 版本信息
      echo "Testing API version endpoint..."
      VERSION_RESPONSE=$(curl -s $BASE_URL/api/version)
      echo "Version response: $VERSION_RESPONSE"
      
      if echo "$VERSION_RESPONSE" | grep -q "version"; then
        echo "✓ Version endpoint test passed"
      else
        echo "✗ Version endpoint test failed"
        exit 1
      fi
      
      # 测试用户列表 API
      echo "Testing users list endpoint..."
      USERS_RESPONSE=$(curl -s -w "%{http_code}" $BASE_URL/api/users)
      HTTP_CODE=${USERS_RESPONSE: -3}
      
      if [ "$HTTP_CODE" = "200" ]; then
        echo "✓ Users endpoint test passed"
      else
        echo "✗ Users endpoint test failed (HTTP $HTTP_CODE)"
        exit 1
      fi
      
      # 测试创建用户 API
      echo "Testing user creation endpoint..."
      CREATE_RESPONSE=$(curl -s -w "%{http_code}" -X POST \
        -H "Content-Type: application/json" \
        -d '{"name":"test-user","email":"test@example.com"}' \
        $BASE_URL/api/users)
      
      HTTP_CODE=${CREATE_RESPONSE: -3}
      
      if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
        echo "✓ User creation test passed"
      else
        echo "✗ User creation test failed (HTTP $HTTP_CODE)"
        exit 1
      fi
      
      echo "API functionality test completed successfully"

6.2.3 数据库连接测试

# templates/tests/database-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-database-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "2"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: database-test
    image: postgres:13
    env:
    - name: PGPASSWORD
      valueFrom:
        secretKeyRef:
          name: {{ .Values.database.secretName }}
          key: password
    - name: PGHOST
      value: {{ .Values.database.host }}
    - name: PGPORT
      value: "{{ .Values.database.port }}"
    - name: PGUSER
      value: {{ .Values.database.username }}
    - name: PGDATABASE
      value: {{ .Values.database.name }}
    command:
    - /bin/bash
    - -c
    - |
      echo "Starting database connectivity test..."
      
      # 测试数据库连接
      echo "Testing database connection..."
      pg_isready -h $PGHOST -p $PGPORT -U $PGUSER
      
      if [ $? -eq 0 ]; then
        echo "✓ Database connection test passed"
      else
        echo "✗ Database connection test failed"
        exit 1
      fi
      
      # 测试数据库查询
      echo "Testing database query..."
      RESULT=$(psql -h $PGHOST -p $PGPORT -U $PGUSER -d $PGDATABASE -t -c "SELECT 1;")
      
      if [ "$(echo $RESULT | tr -d ' ')" = "1" ]; then
        echo "✓ Database query test passed"
      else
        echo "✗ Database query test failed"
        exit 1
      fi
      
      # 测试表结构
      echo "Testing table structure..."
      TABLE_COUNT=$(psql -h $PGHOST -p $PGPORT -U $PGUSER -d $PGDATABASE -t -c \
        "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
      
      if [ "$(echo $TABLE_COUNT | tr -d ' ')" -gt "0" ]; then
        echo "✓ Table structure test passed ($TABLE_COUNT tables found)"
      else
        echo "✗ Table structure test failed (no tables found)"
        exit 1
      fi
      
      echo "Database connectivity test completed successfully"

6.2.4 性能测试

# templates/tests/performance-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-performance-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "3"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: performance-test
    image: alpine:3.16
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting performance test..."
      
      # 安装必要工具
      apk add --no-cache curl time
      
      BASE_URL="http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
      
      # 响应时间测试
      echo "Testing response time..."
      RESPONSE_TIME=$(curl -o /dev/null -s -w "%{time_total}" $BASE_URL/health)
      
      echo "Response time: ${RESPONSE_TIME}s"
      
      # 检查响应时间是否在可接受范围内 (< 2秒)
      if [ "$(echo "$RESPONSE_TIME < 2.0" | bc -l 2>/dev/null || echo 0)" = "1" ]; then
        echo "✓ Response time test passed"
      else
        echo "✗ Response time test failed (${RESPONSE_TIME}s > 2.0s)"
        exit 1
      fi
      
      # 并发测试
      echo "Testing concurrent requests..."
      
      # 创建并发请求函数
      test_concurrent() {
        for i in $(seq 1 10); do
          curl -s $BASE_URL/health > /dev/null &
        done
        wait
      }
      
      # 执行并发测试
      START_TIME=$(date +%s.%N)
      test_concurrent
      END_TIME=$(date +%s.%N)
      
      DURATION=$(echo "$END_TIME - $START_TIME" | bc -l 2>/dev/null || echo "1")
      echo "Concurrent test duration: ${DURATION}s"
      
      # 检查并发性能
      if [ "$(echo "$DURATION < 5.0" | bc -l 2>/dev/null || echo 0)" = "1" ]; then
        echo "✓ Concurrent test passed"
      else
        echo "✗ Concurrent test failed (${DURATION}s > 5.0s)"
        exit 1
      fi
      
      echo "Performance test completed successfully"

6.3 高级测试策略

6.3.1 集成测试

# templates/tests/integration-test.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ include "myapp.fullname" . }}-integration-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: integration-test
        image: node:16-alpine
        workingDir: /app
        command:
        - /bin/sh
        - -c
        - |
          echo "Starting integration test..."
          
          # 安装测试依赖
          npm install -g newman
          
          # 创建 Postman 集合
          cat > collection.json << 'EOF'
          {
            "info": {
              "name": "{{ include "myapp.fullname" . }} Integration Tests",
              "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
            },
            "item": [
              {
                "name": "Health Check",
                "request": {
                  "method": "GET",
                  "header": [],
                  "url": {
                    "raw": "{{base_url}}/health",
                    "host": ["{{base_url}}"],
                    "path": ["health"]
                  }
                },
                "event": [
                  {
                    "listen": "test",
                    "script": {
                      "exec": [
                        "pm.test('Status code is 200', function () {",
                        "    pm.response.to.have.status(200);",
                        "});",
                        "pm.test('Response contains status', function () {",
                        "    pm.expect(pm.response.text()).to.include('status');",
                        "});"
                      ]
                    }
                  }
                ]
              },
              {
                "name": "User CRUD Operations",
                "item": [
                  {
                    "name": "Create User",
                    "request": {
                      "method": "POST",
                      "header": [
                        {
                          "key": "Content-Type",
                          "value": "application/json"
                        }
                      ],
                      "body": {
                        "mode": "raw",
                        "raw": "{\n  \"name\": \"Integration Test User\",\n  \"email\": \"integration@test.com\"\n}"
                      },
                      "url": {
                        "raw": "{{base_url}}/api/users",
                        "host": ["{{base_url}}"],
                        "path": ["api", "users"]
                      }
                    },
                    "event": [
                      {
                        "listen": "test",
                        "script": {
                          "exec": [
                            "pm.test('User created successfully', function () {",
                            "    pm.response.to.have.status(201);",
                            "});",
                            "pm.test('Response contains user ID', function () {",
                            "    const response = pm.response.json();",
                            "    pm.expect(response).to.have.property('id');",
                            "    pm.globals.set('user_id', response.id);",
                            "});"
                          ]
                        }
                      }
                    ]
                  },
                  {
                    "name": "Get User",
                    "request": {
                      "method": "GET",
                      "url": {
                        "raw": "{{base_url}}/api/users/{{user_id}}",
                        "host": ["{{base_url}}"],
                        "path": ["api", "users", "{{user_id}}"]
                      }
                    },
                    "event": [
                      {
                        "listen": "test",
                        "script": {
                          "exec": [
                            "pm.test('User retrieved successfully', function () {",
                            "    pm.response.to.have.status(200);",
                            "});",
                            "pm.test('User data is correct', function () {",
                            "    const response = pm.response.json();",
                            "    pm.expect(response.email).to.eql('integration@test.com');",
                            "});"
                          ]
                        }
                      }
                    ]
                  }
                ]
              }
            ]
          }
          EOF
          
          # 创建环境变量
          cat > environment.json << EOF
          {
            "id": "integration-env",
            "name": "Integration Environment",
            "values": [
              {
                "key": "base_url",
                "value": "http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}",
                "enabled": true
              }
            ]
          }
          EOF
          
          # 运行集成测试
          echo "Running Newman collection..."
          newman run collection.json -e environment.json --reporters cli,json --reporter-json-export results.json
          
          # 检查测试结果
          if [ $? -eq 0 ]; then
            echo "✓ Integration test passed"
          else
            echo "✗ Integration test failed"
            cat results.json
            exit 1
          fi
          
          echo "Integration test completed successfully"

6.3.2 端到端测试

# templates/tests/e2e-test.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ include "myapp.fullname" . }}-e2e-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "10"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: e2e-test
        image: cypress/included:10.11.0
        workingDir: /app
        command:
        - /bin/sh
        - -c
        - |
          echo "Starting end-to-end test..."
          
          # 创建 Cypress 配置
          cat > cypress.config.js << 'EOF'
          const { defineConfig } = require('cypress')
          
          module.exports = defineConfig({
            e2e: {
              baseUrl: 'http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}',
              supportFile: false,
              video: false,
              screenshot: false,
              setupNodeEvents(on, config) {
                // implement node event listeners here
              },
            },
          })
          EOF
          
          # 创建测试目录
          mkdir -p cypress/e2e
          
          # 创建 E2E 测试
          cat > cypress/e2e/app.cy.js << 'EOF'
          describe('Application E2E Tests', () => {
            beforeEach(() => {
              cy.visit('/')
            })
          
            it('should load the homepage', () => {
              cy.get('body').should('be.visible')
              cy.title().should('not.be.empty')
            })
          
            it('should navigate to user management', () => {
              cy.get('[data-cy="users-link"]').click()
              cy.url().should('include', '/users')
              cy.get('[data-cy="users-table"]').should('be.visible')
            })
          
            it('should create a new user', () => {
              cy.get('[data-cy="users-link"]').click()
              cy.get('[data-cy="add-user-btn"]').click()
              
              cy.get('[data-cy="user-name-input"]').type('E2E Test User')
              cy.get('[data-cy="user-email-input"]').type('e2e@test.com')
              cy.get('[data-cy="save-user-btn"]').click()
              
              cy.get('[data-cy="success-message"]').should('contain', 'User created successfully')
              cy.get('[data-cy="users-table"]').should('contain', 'E2E Test User')
            })
          
            it('should handle API errors gracefully', () => {
              // 模拟网络错误
              cy.intercept('POST', '/api/users', { forceNetworkError: true }).as('createUserError')
              
              cy.get('[data-cy="users-link"]').click()
              cy.get('[data-cy="add-user-btn"]').click()
              
              cy.get('[data-cy="user-name-input"]').type('Error Test User')
              cy.get('[data-cy="user-email-input"]').type('error@test.com')
              cy.get('[data-cy="save-user-btn"]').click()
              
              cy.wait('@createUserError')
              cy.get('[data-cy="error-message"]').should('be.visible')
            })
          
            it('should be responsive on mobile', () => {
              cy.viewport('iphone-6')
              cy.get('[data-cy="mobile-menu-btn"]').should('be.visible')
              cy.get('[data-cy="mobile-menu-btn"]').click()
              cy.get('[data-cy="mobile-nav"]').should('be.visible')
            })
          })
          EOF
          
          # 等待应用就绪
          echo "Waiting for application to be ready..."
          until curl -f http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health; do
            echo "Waiting for application..."
            sleep 5
          done
          
          # 运行 Cypress 测试
          echo "Running Cypress E2E tests..."
          npx cypress run --headless --browser chrome
          
          if [ $? -eq 0 ]; then
            echo "✓ E2E test passed"
          else
            echo "✗ E2E test failed"
            exit 1
          fi
          
          echo "End-to-end test completed successfully"

6.3.3 安全测试

# templates/tests/security-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-security-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "4"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: security-test
    image: alpine:3.16
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting security test..."
      
      # 安装必要工具
      apk add --no-cache curl nmap
      
      BASE_URL="http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
      
      # 测试 HTTPS 重定向
      echo "Testing HTTPS redirect..."
      HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $BASE_URL)
      
      {{- if .Values.ingress.tls }}
      if [ "$HTTP_RESPONSE" = "301" ] || [ "$HTTP_RESPONSE" = "302" ]; then
        echo "✓ HTTPS redirect test passed"
      else
        echo "✗ HTTPS redirect test failed (HTTP $HTTP_RESPONSE)"
        exit 1
      fi
      {{- else }}
      echo "ℹ HTTPS not configured, skipping redirect test"
      {{- end }}
      
      # 测试安全头
      echo "Testing security headers..."
      HEADERS=$(curl -s -I $BASE_URL/health)
      
      # 检查 X-Frame-Options
      if echo "$HEADERS" | grep -i "x-frame-options"; then
        echo "✓ X-Frame-Options header found"
      else
        echo "⚠ X-Frame-Options header missing"
      fi
      
      # 检查 X-Content-Type-Options
      if echo "$HEADERS" | grep -i "x-content-type-options"; then
        echo "✓ X-Content-Type-Options header found"
      else
        echo "⚠ X-Content-Type-Options header missing"
      fi
      
      # 检查 X-XSS-Protection
      if echo "$HEADERS" | grep -i "x-xss-protection"; then
        echo "✓ X-XSS-Protection header found"
      else
        echo "⚠ X-XSS-Protection header missing"
      fi
      
      # 测试敏感信息泄露
      echo "Testing for information disclosure..."
      
      # 检查服务器信息泄露
      if echo "$HEADERS" | grep -i "server:" | grep -v "nginx\|apache"; then
        echo "⚠ Server information may be disclosed"
      else
        echo "✓ Server information disclosure test passed"
      fi
      
      # 测试常见漏洞端点
      echo "Testing common vulnerability endpoints..."
      
      VULN_ENDPOINTS=(
        "/admin"
        "/.env"
        "/config"
        "/debug"
        "/actuator"
      )
      
      for endpoint in "${VULN_ENDPOINTS[@]}"; do
        RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $BASE_URL$endpoint)
        if [ "$RESPONSE" = "200" ]; then
          echo "⚠ Potentially sensitive endpoint accessible: $endpoint"
        else
          echo "✓ Endpoint $endpoint properly protected"
        fi
      done
      
      # 测试 SQL 注入防护
      echo "Testing SQL injection protection..."
      INJECTION_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
        "$BASE_URL/api/users?id=1' OR '1'='1")
      
      if [ "$INJECTION_RESPONSE" = "400" ] || [ "$INJECTION_RESPONSE" = "403" ]; then
        echo "✓ SQL injection protection test passed"
      else
        echo "⚠ SQL injection protection may be insufficient"
      fi
      
      echo "Security test completed"

6.4 测试配置和管理

6.4.1 测试配置文件

# values.yaml 中的测试配置
testing:
  enabled: true
  
  # 测试超时设置
  timeout: 300
  
  # 测试并行度
  parallelism: 1
  
  # 测试环境配置
  environment:
    testData:
      users:
        - name: "Test User 1"
          email: "test1@example.com"
        - name: "Test User 2"
          email: "test2@example.com"
    
    # 测试数据库配置
    database:
      host: "test-db"
      port: 5432
      name: "testdb"
      username: "testuser"
      secretName: "test-db-secret"
  
  # 性能测试配置
  performance:
    responseTimeThreshold: 2.0
    concurrentRequests: 10
    testDuration: 60
  
  # 安全测试配置
  security:
    enableVulnerabilityScanning: true
    checkSecurityHeaders: true
    testCommonVulnerabilities: true
  
  # 集成测试配置
  integration:
    postmanCollection: "integration-tests.json"
    environment: "test-environment.json"
  
  # E2E 测试配置
  e2e:
    browser: "chrome"
    headless: true
    viewport:
      width: 1280
      height: 720

6.4.2 条件测试

# templates/tests/conditional-test.yaml
{{- if .Values.testing.enabled }}
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-conditional-test"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  restartPolicy: Never
  activeDeadlineSeconds: {{ .Values.testing.timeout | default 300 }}
  containers:
  - name: conditional-test
    image: alpine:3.16
    command:
    - /bin/sh
    - -c
    - |
      echo "Running conditional tests..."
      
      {{- if .Values.database.enabled }}
      echo "Database is enabled, running database tests..."
      # 数据库相关测试
      {{- end }}
      
      {{- if .Values.redis.enabled }}
      echo "Redis is enabled, running cache tests..."
      # Redis 相关测试
      {{- end }}
      
      {{- if .Values.ingress.enabled }}
      echo "Ingress is enabled, running ingress tests..."
      # Ingress 相关测试
      {{- end }}
      
      {{- if .Values.monitoring.enabled }}
      echo "Monitoring is enabled, running monitoring tests..."
      # 监控相关测试
      {{- end }}
      
      echo "Conditional tests completed"
{{- end }}

6.4.3 测试数据管理

# templates/tests/test-data-setup.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ include "myapp.fullname" . }}-test-data-setup"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "-10"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: test-data-setup
        image: postgres:13
        env:
        - name: PGPASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ .Values.testing.environment.database.secretName }}
              key: password
        command:
        - /bin/bash
        - -c
        - |
          echo "Setting up test data..."
          
          # 等待数据库就绪
          until pg_isready -h {{ .Values.testing.environment.database.host }} \
                           -p {{ .Values.testing.environment.database.port }} \
                           -U {{ .Values.testing.environment.database.username }}; do
            echo "Waiting for test database..."
            sleep 2
          done
          
          # 创建测试数据
          psql -h {{ .Values.testing.environment.database.host }} \
               -p {{ .Values.testing.environment.database.port }} \
               -U {{ .Values.testing.environment.database.username }} \
               -d {{ .Values.testing.environment.database.name }} << 'EOF'
          
          -- 清理现有测试数据
          DELETE FROM users WHERE email LIKE '%@example.com';
          
          -- 插入测试用户
          {{- range .Values.testing.environment.testData.users }}
          INSERT INTO users (name, email, created_at) 
          VALUES ('{{ .name }}', '{{ .email }}', NOW())
          ON CONFLICT (email) DO NOTHING;
          {{- end }}
          
          -- 创建测试角色
          INSERT INTO roles (name, description) 
          VALUES ('test-role', 'Test Role for Integration Tests')
          ON CONFLICT (name) DO NOTHING;
          
          -- 设置测试权限
          INSERT INTO permissions (name, resource, action) 
          VALUES 
            ('test-read', 'users', 'read'),
            ('test-write', 'users', 'write')
          ON CONFLICT (name) DO NOTHING;
          
          EOF
          
          echo "Test data setup completed"

6.5 测试执行和管理

6.5.1 基本测试命令

# 运行所有测试
helm test myapp

# 运行测试并显示详细输出
helm test myapp --logs

# 运行特定测试
helm test myapp --filter name=health-test

# 运行测试并保持 Pod(用于调试)
helm test myapp --timeout 600s

# 清理测试资源
kubectl delete pods -l "helm.sh/hook=test"

6.5.2 测试脚本

#!/bin/bash
# scripts/run-tests.sh

set -e

RELEASE_NAME=${1:-myapp}
NAMESPACE=${2:-default}
TIMEOUT=${3:-300s}

echo "Running Helm tests for release: $RELEASE_NAME"
echo "Namespace: $NAMESPACE"
echo "Timeout: $TIMEOUT"

# 检查 release 状态
echo "Checking release status..."
helm status $RELEASE_NAME -n $NAMESPACE

if [ $? -ne 0 ]; then
  echo "Error: Release $RELEASE_NAME not found or not deployed"
  exit 1
fi

# 等待所有 Pod 就绪
echo "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod \
  -l "app.kubernetes.io/instance=$RELEASE_NAME" \
  -n $NAMESPACE \
  --timeout=$TIMEOUT

# 运行测试
echo "Running tests..."
helm test $RELEASE_NAME -n $NAMESPACE --timeout $TIMEOUT --logs

TEST_RESULT=$?

# 收集测试结果
echo "Collecting test results..."
mkdir -p test-results

# 获取测试 Pod 日志
for pod in $(kubectl get pods -l "helm.sh/hook=test" -n $NAMESPACE -o name); do
  pod_name=$(basename $pod)
  echo "Collecting logs for $pod_name..."
  kubectl logs $pod -n $NAMESPACE > test-results/${pod_name}.log
done

# 获取测试 Pod 状态
kubectl get pods -l "helm.sh/hook=test" -n $NAMESPACE -o yaml > test-results/test-pods.yaml

# 生成测试报告
echo "Generating test report..."
cat > test-results/test-report.md << EOF
# Test Report for $RELEASE_NAME

**Date:** $(date)
**Release:** $RELEASE_NAME
**Namespace:** $NAMESPACE
**Result:** $([ $TEST_RESULT -eq 0 ] && echo "PASSED" || echo "FAILED")

## Test Summary

\`\`\`
$(kubectl get pods -l "helm.sh/hook=test" -n $NAMESPACE)
\`\`\`

## Test Details

EOF

# 添加每个测试的详细信息
for pod in $(kubectl get pods -l "helm.sh/hook=test" -n $NAMESPACE -o jsonpath='{.items[*].metadata.name}'); do
  echo "### $pod" >> test-results/test-report.md
  echo "" >> test-results/test-report.md
  echo "\`\`\`" >> test-results/test-report.md
  kubectl logs $pod -n $NAMESPACE >> test-results/test-report.md
  echo "\`\`\`" >> test-results/test-report.md
  echo "" >> test-results/test-report.md
done

# 清理测试资源(可选)
if [ "$CLEANUP_TESTS" = "true" ]; then
  echo "Cleaning up test resources..."
  kubectl delete pods -l "helm.sh/hook=test" -n $NAMESPACE
fi

echo "Test execution completed. Results saved in test-results/"

if [ $TEST_RESULT -eq 0 ]; then
  echo "✓ All tests passed"
  exit 0
else
  echo "✗ Some tests failed"
  exit 1
fi

6.5.3 CI/CD 集成

# .github/workflows/helm-test.yml
name: Helm Chart Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Setup Kubernetes
      uses: helm/kind-action@v1.4.0
      with:
        cluster_name: test-cluster
    
    - name: Setup Helm
      uses: azure/setup-helm@v3
      with:
        version: '3.10.0'
    
    - name: Add Helm repositories
      run: |
        helm repo add bitnami https://charts.bitnami.com/bitnami
        helm repo update
    
    - name: Install dependencies
      run: |
        helm dependency update
    
    - name: Lint Chart
      run: |
        helm lint .
    
    - name: Template Chart
      run: |
        helm template test-release . --debug
    
    - name: Install Chart
      run: |
        helm install test-release . \
          --wait \
          --timeout 300s \
          --set testing.enabled=true
    
    - name: Run Tests
      run: |
        chmod +x scripts/run-tests.sh
        ./scripts/run-tests.sh test-release default 600s
    
    - name: Upload Test Results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: test-results/
    
    - name: Cleanup
      if: always()
      run: |
        helm uninstall test-release || true
        kubectl delete pods -l "helm.sh/hook=test" || true

6.6 测试调试和故障排除

6.6.1 测试调试技巧

# 查看测试 Pod 状态
kubectl get pods -l "helm.sh/hook=test"

# 查看测试 Pod 详细信息
kubectl describe pod myapp-health-test

# 查看测试日志
kubectl logs myapp-health-test

# 进入测试 Pod 进行调试
kubectl exec -it myapp-health-test -- /bin/sh

# 手动运行测试命令
kubectl run debug-test --rm -it --image=curlimages/curl -- /bin/sh

6.6.2 常见问题解决

# templates/tests/debug-helper.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-debug-helper"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: debug-helper
    image: alpine:3.16
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting debug helper..."
      
      # 安装调试工具
      apk add --no-cache curl dig nmap netcat-openbsd
      
      echo "=== Network Connectivity Tests ==="
      
      # 测试 DNS 解析
      echo "Testing DNS resolution..."
      nslookup {{ include "myapp.fullname" . }}
      
      # 测试端口连通性
      echo "Testing port connectivity..."
      nc -zv {{ include "myapp.fullname" . }} {{ .Values.service.port }}
      
      # 测试服务发现
      echo "Testing service discovery..."
      dig {{ include "myapp.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
      
      echo "=== Application Status ==="
      
      # 检查应用健康状态
      echo "Checking application health..."
      curl -v http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health || true
      
      # 检查应用版本
      echo "Checking application version..."
      curl -s http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/version || true
      
      echo "=== Kubernetes Resources ==="
      
      # 检查相关资源
      echo "Checking pods..."
      kubectl get pods -l "app.kubernetes.io/instance={{ .Release.Name }}" -o wide
      
      echo "Checking services..."
      kubectl get services -l "app.kubernetes.io/instance={{ .Release.Name }}"
      
      echo "Checking endpoints..."
      kubectl get endpoints {{ include "myapp.fullname" . }}
      
      echo "=== Environment Information ==="
      
      echo "Release Name: {{ .Release.Name }}"
      echo "Release Namespace: {{ .Release.Namespace }}"
      echo "Chart Version: {{ .Chart.Version }}"
      echo "App Version: {{ .Chart.AppVersion }}"
      
      echo "Debug helper completed"

6.6.3 性能分析

# templates/tests/performance-analysis.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "myapp.fullname" . }}-performance-analysis"
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-weight": "20"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: performance-analysis
    image: alpine:3.16
    command:
    - /bin/sh
    - -c
    - |
      echo "Starting performance analysis..."
      
      # 安装工具
      apk add --no-cache curl apache2-utils bc
      
      BASE_URL="http://{{ include "myapp.fullname" . }}:{{ .Values.service.port }}"
      
      echo "=== Response Time Analysis ==="
      
      # 单次请求分析
      echo "Single request analysis:"
      curl -w "\nTime breakdown:\n  DNS lookup:    %{time_namelookup}s\n  Connect:       %{time_connect}s\n  Pre-transfer:  %{time_pretransfer}s\n  Start transfer:%{time_starttransfer}s\n  Total:         %{time_total}s\n" \
           -o /dev/null -s $BASE_URL/health
      
      echo "\n=== Load Testing ==="
      
      # 使用 ab 进行负载测试
      echo "Running load test (100 requests, 10 concurrent):"
      ab -n 100 -c 10 -g results.tsv $BASE_URL/health
      
      # 分析结果
      echo "\n=== Performance Metrics ==="
      
      # 计算平均响应时间
      AVG_TIME=$(awk 'NR>1 {sum+=$9; count++} END {print sum/count}' results.tsv)
      echo "Average response time: ${AVG_TIME}ms"
      
      # 计算 95th 百分位
      P95_TIME=$(awk 'NR>1 {print $9}' results.tsv | sort -n | awk '{a[NR]=$1} END {print a[int(NR*0.95)]}')
      echo "95th percentile: ${P95_TIME}ms"
      
      # 检查性能阈值
      THRESHOLD={{ .Values.testing.performance.responseTimeThreshold | default 2000 }}
      
      if [ "$(echo "$AVG_TIME < $THRESHOLD" | bc -l)" = "1" ]; then
        echo "✓ Performance test passed (avg: ${AVG_TIME}ms < ${THRESHOLD}ms)"
      else
        echo "✗ Performance test failed (avg: ${AVG_TIME}ms >= ${THRESHOLD}ms)"
        exit 1
      fi
      
      echo "Performance analysis completed"

6.7 本章小结

核心概念回顾

  1. Helm 测试框架:基于 Kubernetes Pod 的测试执行环境
  2. 测试类型:健康检查、API 测试、集成测试、E2E 测试、安全测试、性能测试
  3. 测试 Hook:使用 helm.sh/hook: test 注解标识测试资源
  4. 测试权重:控制测试执行顺序
  5. 测试管理:测试配置、执行、调试和结果收集

技术要点总结

  • 测试通过特殊的 Pod 资源实现
  • 支持多种测试策略和场景
  • 可以集成到 CI/CD 流程中
  • 提供丰富的调试和故障排除工具
  • 支持条件测试和参数化配置

最佳实践

  1. 分层测试:从单元测试到集成测试再到 E2E 测试
  2. 测试隔离:确保测试之间不相互影响
  3. 测试数据管理:合理管理测试数据的创建和清理
  4. 性能监控:监控测试执行时间和资源使用
  5. 持续集成:将测试集成到 CI/CD 流程中
  6. 文档化:详细记录测试用例和预期结果

下一章预告

下一章我们将学习「Chart 仓库与分发」,探讨如何创建和管理 Helm Chart 仓库,包括公共仓库、私有仓库、Chart 打包、版本管理、安全签名等内容,以及如何构建企业级的 Chart 分发体系。