Skip to content

8.9 自动化测试实战 🤖

"自动化测试不是为了取代人工测试,而是为了让开发者从重复劳动中解放出来,专注于创造价值。"

想象自动化测试就像是给你的代码配备了一支24小时待命的质检团队。每当有新代码提交,这支团队就会立刻行动,全面检查代码质量,确保没有任何问题溜过去。

CI/CD集成:让测试自动运转 ⚙️

GitHub Actions工作流配置

yaml
# 🌟 完整的CI/CD工作流配置
# .github/workflows/mcp-ci-cd.yml
name: MCP Server CI/CD Pipeline

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

env:
  NODE_VERSION: '18'
  PYTHON_VERSION: '3.11'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 1. 代码质量检查
  code-quality:
    runs-on: ubuntu-latest
    name: 代码质量检查
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
      with:
        fetch-depth: 0  # 获取完整历史用于SonarQube分析
    
    - name: 设置Node.js环境
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: ESLint代码检查
      run: |
        echo "🔍 运行ESLint代码检查..."
        npm run lint:check
        
    - name: Prettier格式检查
      run: |
        echo "💅 检查代码格式..."
        npm run format:check
    
    - name: TypeScript类型检查
      run: |
        echo "🔤 运行TypeScript类型检查..."
        npm run type-check
    
    - name: 安全漏洞扫描
      run: |
        echo "🛡️ 扫描安全漏洞..."
        npm audit --audit-level=moderate
        
    - name: 依赖许可证检查
      run: |
        echo "📄 检查依赖许可证..."
        npx license-checker --onlyAllow "MIT;Apache-2.0;BSD-3-Clause;ISC"
    
    - name: 上传代码质量报告
      uses: github/super-linter@v4
      env:
        DEFAULT_BRANCH: main
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        VALIDATE_TYPESCRIPT_ES: true
        VALIDATE_JAVASCRIPT_ES: true
        VALIDATE_JSON: true
        VALIDATE_YAML: true

  # 2. 单元测试
  unit-tests:
    runs-on: ubuntu-latest
    name: 单元测试
    needs: code-quality
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
        
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
    
    - name: 设置Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 运行单元测试
      run: |
        echo "🧪 运行单元测试 (Node.js ${{ matrix.node-version }})..."
        npm run test:unit -- --coverage --maxWorkers=2
      env:
        CI: true
    
    - name: 上传测试覆盖率
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info
        flags: unit-tests
        name: codecov-unit-${{ matrix.node-version }}
        fail_ci_if_error: false
    
    - name: 上传测试结果
      uses: dorny/test-reporter@v1
      if: success() || failure()
      with:
        name: 单元测试结果 (Node.js ${{ matrix.node-version }})
        path: 'test-results/junit.xml'
        reporter: jest-junit

  # 3. 集成测试
  integration-tests:
    runs-on: ubuntu-latest
    name: 集成测试
    needs: unit-tests
    
    services:
      # Redis服务用于缓存测试
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      # PostgreSQL服务用于数据库测试
      postgres:
        image: postgres:15-alpine
        env:
          POSTGRES_DB: mcp_test
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_password
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
    
    - name: 设置Node.js环境
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 等待服务就绪
      run: |
        echo "⏳ 等待数据库和缓存服务就绪..."
        timeout 30 bash -c 'until nc -z localhost 5432; do sleep 1; done'
        timeout 30 bash -c 'until nc -z localhost 6379; do sleep 1; done'
        echo "✅ 所有服务已就绪"
    
    - name: 初始化测试数据库
      run: |
        echo "📊 初始化测试数据库..."
        npm run db:migrate:test
        npm run db:seed:test
      env:
        DATABASE_URL: postgresql://test_user:test_password@localhost:5432/mcp_test
        REDIS_URL: redis://localhost:6379
    
    - name: 运行集成测试
      run: |
        echo "🔗 运行集成测试..."
        npm run test:integration -- --maxWorkers=1
      env:
        CI: true
        DATABASE_URL: postgresql://test_user:test_password@localhost:5432/mcp_test
        REDIS_URL: redis://localhost:6379
    
    - name: 上传集成测试覆盖率
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/integration-lcov.info
        flags: integration-tests
        name: codecov-integration
        fail_ci_if_error: false

  # 4. 端到端测试
  e2e-tests:
    runs-on: ubuntu-latest
    name: 端到端测试
    needs: integration-tests
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
    
    - name: 设置Node.js环境
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 安装Playwright浏览器
      run: npx playwright install --with-deps
    
    - name: 构建应用
      run: |
        echo "🏗️ 构建生产版本..."
        npm run build
    
    - name: 启动应用服务器
      run: |
        echo "🚀 启动应用服务器..."
        npm run start:test &
        # 等待服务器启动
        timeout 60 bash -c 'until curl -f http://localhost:3000/health; do sleep 2; done'
        echo "✅ 应用服务器已启动"
      env:
        NODE_ENV: test
        PORT: 3000
    
    - name: 运行端到端测试
      run: |
        echo "🌐 运行端到端测试..."
        npm run test:e2e
      env:
        BASE_URL: http://localhost:3000
    
    - name: 上传Playwright报告
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 7
    
    - name: 上传E2E测试截图
      uses: actions/upload-artifact@v3
      if: failure()
      with:
        name: e2e-screenshots
        path: test-results/
        retention-days: 7

  # 5. 性能测试
  performance-tests:
    runs-on: ubuntu-latest
    name: 性能测试
    needs: integration-tests
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
    
    - name: 设置Node.js环境
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 构建应用
      run: npm run build
    
    - name: 启动应用
      run: |
        npm run start:prod &
        timeout 60 bash -c 'until curl -f http://localhost:3000/health; do sleep 2; done'
      env:
        NODE_ENV: production
        PORT: 3000
    
    - name: 运行负载测试
      run: |
        echo "⚡ 运行负载测试..."
        npm run test:load
    
    - name: 运行压力测试
      run: |
        echo "💪 运行压力测试..."
        npm run test:stress
    
    - name: 分析性能指标
      run: |
        echo "📊 分析性能指标..."
        npm run analyze:performance
    
    - name: 上传性能报告
      uses: actions/upload-artifact@v3
      with:
        name: performance-report
        path: performance-report/
        retention-days: 30

  # 6. 安全测试
  security-tests:
    runs-on: ubuntu-latest
    name: 安全测试
    needs: code-quality
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
    
    - name: 运行Trivy漏洞扫描
      uses: aquasecurity/trivy-action@master
      with:
        scan-type: 'fs'
        scan-ref: '.'
        format: 'sarif'
        output: 'trivy-results.sarif'
    
    - name: 上传Trivy扫描结果
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'
    
    - name: 运行Semgrep安全分析
      run: |
        echo "🔒 运行Semgrep安全分析..."
        python -m pip install semgrep
        semgrep --config=auto --json --output=semgrep-report.json .
    
    - name: 上传安全测试报告
      uses: actions/upload-artifact@v3
      with:
        name: security-report
        path: |
          trivy-results.sarif
          semgrep-report.json
        retention-days: 30

  # 7. 构建和发布
  build-and-publish:
    runs-on: ubuntu-latest
    name: 构建和发布
    needs: [unit-tests, integration-tests, e2e-tests, security-tests]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    permissions:
      contents: read
      packages: write
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v4
    
    - name: 设置Node.js环境
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: 安装依赖
      run: npm ci
    
    - name: 构建应用
      run: |
        echo "🏗️ 构建生产版本..."
        npm run build
    
    - name: 登录容器注册表
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: 提取元数据
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}
    
    - name: 构建并推送Docker镜像
      uses: docker/build-push-action@v4
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
    
    - name: 部署到暂存环境
      if: github.ref == 'refs/heads/main'
      run: |
        echo "🚀 部署到暂存环境..."
        # 这里可以添加部署脚本
        echo "部署完成!"
      env:
        DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        STAGING_SERVER: ${{ secrets.STAGING_SERVER }}

  # 8. 通知和报告
  notify:
    runs-on: ubuntu-latest
    name: 通知和报告
    needs: [build-and-publish]
    if: always()
    
    steps:
    - name: 获取工作流状态
      id: workflow-status
      run: |
        if [[ "${{ needs.build-and-publish.result }}" == "success" ]]; then
          echo "status=success" >> $GITHUB_OUTPUT
          echo "message=🎉 CI/CD流水线执行成功!" >> $GITHUB_OUTPUT
          echo "color=good" >> $GITHUB_OUTPUT
        else
          echo "status=failure" >> $GITHUB_OUTPUT
          echo "message=❌ CI/CD流水线执行失败!" >> $GITHUB_OUTPUT
          echo "color=danger" >> $GITHUB_OUTPUT
        fi
    
    - name: 发送Slack通知
      uses: 8398a7/action-slack@v3
      if: always()
      with:
        status: ${{ steps.workflow-status.outputs.status }}
        text: |
          ${{ steps.workflow-status.outputs.message }}
          
          📊 **执行摘要:**
          • 代码质量检查: ${{ needs.code-quality.result }}
          • 单元测试: ${{ needs.unit-tests.result }}
          • 集成测试: ${{ needs.integration-tests.result }}
          • 端到端测试: ${{ needs.e2e-tests.result }}
          • 安全测试: ${{ needs.security-tests.result }}
          • 构建发布: ${{ needs.build-and-publish.result }}
          
          🔗 查看详情: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
    
    - name: 更新README徽章
      if: github.ref == 'refs/heads/main'
      run: |
        echo "📛 更新README状态徽章..."
        # 可以使用shields.io API更新徽章状态
        echo "徽章已更新"

本地开发环境配置

json
// 🌟 package.json 测试脚本配置
{
  "name": "mcp-server",
  "version": "1.0.0",
  "scripts": {
    // 开发环境脚本
    "dev": "tsx watch src/index.ts",
    "build": "tsc && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
    "start": "node dist/index.js",
    "start:prod": "NODE_ENV=production node dist/index.js",
    "start:test": "NODE_ENV=test node dist/index.js",
    
    // 代码质量脚本
    "lint": "eslint src/ tests/ --ext .ts,.js",
    "lint:fix": "eslint src/ tests/ --ext .ts,.js --fix",
    "lint:check": "eslint src/ tests/ --ext .ts,.js --max-warnings 0",
    "format": "prettier --write 'src/**/*.{ts,js,json}' 'tests/**/*.{ts,js,json}'",
    "format:check": "prettier --check 'src/**/*.{ts,js,json}' 'tests/**/*.{ts,js,json}'",
    "type-check": "tsc --noEmit",
    
    // 测试脚本
    "test": "npm run test:unit && npm run test:integration",
    "test:unit": "jest --testPathPattern=tests/unit --coverage",
    "test:integration": "jest --testPathPattern=tests/integration --runInBand",
    "test:e2e": "playwright test",
    "test:watch": "jest --watch --testPathPattern=tests/unit",
    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --no-cache",
    
    // 性能测试脚本
    "test:load": "k6 run tests/performance/load-test.js",
    "test:stress": "k6 run tests/performance/stress-test.js",
    "analyze:performance": "node scripts/analyze-performance.js",
    
    // 数据库脚本
    "db:migrate": "knex migrate:latest",
    "db:migrate:test": "NODE_ENV=test knex migrate:latest",
    "db:seed": "knex seed:run",
    "db:seed:test": "NODE_ENV=test knex seed:run",
    "db:rollback": "knex migrate:rollback",
    
    // 部署脚本
    "docker:build": "docker build -t mcp-server .",
    "docker:run": "docker run -p 3000:3000 mcp-server",
    "deploy:staging": "node scripts/deploy-staging.js",
    "deploy:prod": "node scripts/deploy-production.js",
    
    // 实用工具脚本
    "clean": "rimraf dist coverage test-results",
    "precommit": "lint-staged",
    "prepare": "husky install",
    "release": "standard-version"
  },
  "devDependencies": {
    // 测试框架
    "@jest/globals": "^29.7.0",
    "jest": "^29.7.0",
    "jest-junit": "^16.0.0",
    "ts-jest": "^29.1.1",
    "@playwright/test": "^1.40.0",
    
    // 代码质量工具
    "@typescript-eslint/eslint-plugin": "^6.12.0",
    "@typescript-eslint/parser": "^6.12.0",
    "eslint": "^8.54.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-jest": "^27.6.0",
    "prettier": "^3.1.0",
    
    // Git钩子
    "husky": "^8.0.3",
    "lint-staged": "^15.1.0",
    
    // 构建工具
    "typescript": "^5.3.2",
    "tsx": "^4.6.0",
    "esbuild": "^0.19.8",
    "rimraf": "^5.0.5",
    
    // 性能测试
    "k6": "^0.48.0",
    
    // 其他工具
    "standard-version": "^9.5.0",
    "license-checker": "^25.0.1"
  },
  "lint-staged": {
    "*.{ts,js}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml}": [
      "prettier --write"
    ]
  },
  "jest": {
    "preset": "ts-jest",
    "testEnvironment": "node",
    "roots": ["<rootDir>/src", "<rootDir>/tests"],
    "testMatch": [
      "**/__tests__/**/*.ts",
      "**/?(*.)+(spec|test).ts"
    ],
    "collectCoverageFrom": [
      "src/**/*.ts",
      "!src/**/*.d.ts",
      "!src/types/**/*",
      "!src/index.ts"
    ],
    "coverageReporters": [
      "text",
      "lcov",
      "html",
      "json-summary"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    },
    "setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
    "reporters": [
      "default",
      ["jest-junit", {
        "outputDirectory": "test-results",
        "outputName": "junit.xml"
      }]
    ]
  }
}

高级测试配置

typescript
// 🌟 Jest测试环境配置
// tests/setup.ts
import { jest } from '@jest/globals';
import { setupServer } from 'msw/node';
import { rest } from 'msw';

// 全局测试设置
global.console = {
  ...console,
  // 在测试中静默某些日志
  log: jest.fn(),
  debug: jest.fn(),
  info: jest.fn(),
  warn: console.warn,
  error: console.error,
};

// Mock服务器设置
const server = setupServer(
  // 模拟天气API
  rest.get('https://api.weather.com/v1/weather', (req, res, ctx) => {
    const location = req.url.searchParams.get('location');
    return res(
      ctx.json({
        location,
        temperature: 25,
        description: '晴天',
        humidity: 60,
        timestamp: new Date().toISOString()
      })
    );
  }),
  
  // 模拟认证API
  rest.post('https://api.auth.com/v1/token', (req, res, ctx) => {
    return res(
      ctx.json({
        access_token: 'mock_token_12345',
        token_type: 'Bearer',
        expires_in: 3600
      })
    );
  })
);

// 测试生命周期钩子
beforeAll(() => {
  console.log('🚀 启动测试环境...');
  server.listen({ onUnhandledRequest: 'error' });
});

beforeEach(() => {
  // 重置所有Mock
  jest.clearAllMocks();
});

afterEach(() => {
  // 重置服务器处理器
  server.resetHandlers();
});

afterAll(() => {
  console.log('🛑 关闭测试环境...');
  server.close();
});

// 全局测试工具
export const testUtils = {
  // 等待异步操作完成
  waitFor: (condition: () => boolean, timeout = 5000): Promise<void> => {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      
      const check = () => {
        if (condition()) {
          resolve();
        } else if (Date.now() - startTime > timeout) {
          reject(new Error(`等待超时:${timeout}ms`));
        } else {
          setTimeout(check, 100);
        }
      };
      
      check();
    });
  },
  
  // 创建模拟MCP客户端
  createMockClient: () => ({
    connect: jest.fn().mockResolvedValue(true),
    disconnect: jest.fn().mockResolvedValue(undefined),
    callTool: jest.fn(),
    listTools: jest.fn().mockResolvedValue({
      tools: [
        {
          name: 'get_weather',
          description: '获取天气信息',
          inputSchema: {
            type: 'object',
            properties: {
              location: { type: 'string' }
            }
          }
        }
      ]
    })
  }),
  
  // 模拟数据库连接
  createMockDatabase: () => ({
    query: jest.fn(),
    transaction: jest.fn(),
    close: jest.fn()
  })
};

// 测试数据工厂
export const testDataFactory = {
  createUser: (overrides = {}) => ({
    id: 'user_123',
    username: 'testuser',
    email: 'test@example.com',
    createdAt: new Date(),
    ...overrides
  }),
  
  createWeatherData: (overrides = {}) => ({
    location: '北京',
    temperature: 25,
    description: '晴天',
    humidity: 60,
    timestamp: new Date(),
    ...overrides
  }),
  
  createToolCall: (overrides = {}) => ({
    toolName: 'get_weather',
    parameters: { location: '北京' },
    requestId: 'req_123',
    timestamp: new Date(),
    ...overrides
  })
};

Playwright端到端测试配置

typescript
// 🌟 Playwright配置
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'test-results/e2e-results.xml' }],
    ['github']
  ],
  
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  webServer: {
    command: 'npm run start:test',
    url: 'http://localhost:3000/health',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,
  },
});

// tests/e2e/mcp-workflow.spec.ts
import { test, expect } from '@playwright/test';

test.describe('MCP工作流测试', () => {
  test.beforeEach(async ({ page }) => {
    // 每个测试前的设置
    await page.goto('/');
  });

  test('用户完整工作流', async ({ page }) => {
    // 1. 用户登录
    await page.click('[data-testid="login-button"]');
    await page.fill('[data-testid="username"]', 'testuser');
    await page.fill('[data-testid="password"]', 'testpass');
    await page.click('[data-testid="submit-login"]');
    
    // 验证登录成功
    await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
    
    // 2. 导航到工具页面
    await page.click('[data-testid="tools-nav"]');
    await expect(page.locator('[data-testid="tools-list"]')).toBeVisible();
    
    // 3. 选择天气工具
    await page.click('[data-testid="tool-weather"]');
    await expect(page.locator('[data-testid="tool-form"]')).toBeVisible();
    
    // 4. 填写参数并执行
    await page.fill('[data-testid="location-input"]', '北京');
    await page.click('[data-testid="execute-button"]');
    
    // 5. 验证结果
    await expect(page.locator('[data-testid="tool-result"]')).toBeVisible();
    await expect(page.locator('[data-testid="tool-result"]')).toContainText('北京');
    await expect(page.locator('[data-testid="tool-result"]')).toContainText('温度');
    
    // 6. 验证历史记录
    await page.click('[data-testid="history-nav"]');
    await expect(page.locator('[data-testid="history-list"]')).toBeVisible();
    await expect(page.locator('[data-testid="history-item"]:first-child')).toContainText('get_weather');
  });
  
  test('错误处理流程', async ({ page }) => {
    // 测试错误场景
    await page.goto('/tools');
    await page.click('[data-testid="tool-weather"]');
    
    // 不填写参数直接提交
    await page.click('[data-testid="execute-button"]');
    
    // 验证错误提示
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="error-message"]')).toContainText('地点不能为空');
  });
  
  test('响应式设计测试', async ({ page, isMobile }) => {
    if (isMobile) {
      // 移动端特定测试
      await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
      
      // 测试侧边栏
      await page.click('[data-testid="menu-toggle"]');
      await expect(page.locator('[data-testid="sidebar"]')).toBeVisible();
    } else {
      // 桌面端测试
      await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
    }
  });
});

性能测试配置

javascript
// 🌟 K6性能测试脚本
// tests/performance/load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Rate, Trend } from 'k6/metrics';

// 自定义指标
const apiCalls = new Counter('api_calls_total');
const apiFailures = new Rate('api_failures');
const apiDuration = new Trend('api_duration');

// 测试配置
export const options = {
  stages: [
    { duration: '2m', target: 10 },   // 预热:2分钟达到10用户
    { duration: '5m', target: 50 },   // 负载:5分钟保持50用户
    { duration: '2m', target: 100 },  // 峰值:2分钟达到100用户
    { duration: '5m', target: 100 },  // 峰值:5分钟保持100用户
    { duration: '2m', target: 0 },    // 降级:2分钟降到0用户
  ],
  thresholds: {
    http_req_duration: ['p(95)<2000'], // 95%的请求在2秒内完成
    http_req_failed: ['rate<0.05'],    // 错误率低于5%
    api_duration: ['p(99)<3000'],      // 99%的API调用在3秒内完成
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export default function() {
  const testScenarios = [
    () => testHealthCheck(),
    () => testWeatherTool(),
    () => testToolsList(),
    () => testAuthentication(),
  ];
  
  // 随机选择测试场景
  const scenario = testScenarios[Math.floor(Math.random() * testScenarios.length)];
  scenario();
  
  sleep(1); // 用户思考时间
}

function testHealthCheck() {
  const response = http.get(`${BASE_URL}/health`);
  
  check(response, {
    '健康检查状态码为200': (r) => r.status === 200,
    '健康检查响应时间<100ms': (r) => r.timings.duration < 100,
  });
  
  apiCalls.add(1);
  if (response.status !== 200) {
    apiFailures.add(1);
  }
  apiDuration.add(response.timings.duration);
}

function testWeatherTool() {
  const cities = ['北京', '上海', '广州', '深圳', '杭州'];
  const city = cities[Math.floor(Math.random() * cities.length)];
  
  const payload = JSON.stringify({
    tool: 'get_weather',
    parameters: { location: city }
  });
  
  const params = {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer test_token',
    },
  };
  
  const response = http.post(`${BASE_URL}/api/tools/execute`, payload, params);
  
  check(response, {
    '天气工具状态码为200': (r) => r.status === 200,
    '天气工具返回结果': (r) => {
      try {
        const data = JSON.parse(r.body);
        return data.result && data.result.temperature !== undefined;
      } catch (e) {
        return false;
      }
    },
    '天气工具响应时间<2s': (r) => r.timings.duration < 2000,
  });
  
  apiCalls.add(1);
  if (response.status !== 200) {
    apiFailures.add(1);
  }
  apiDuration.add(response.timings.duration);
}

function testToolsList() {
  const response = http.get(`${BASE_URL}/api/tools`);
  
  check(response, {
    '工具列表状态码为200': (r) => r.status === 200,
    '工具列表包含工具': (r) => {
      try {
        const data = JSON.parse(r.body);
        return Array.isArray(data.tools) && data.tools.length > 0;
      } catch (e) {
        return false;
      }
    },
  });
  
  apiCalls.add(1);
  if (response.status !== 200) {
    apiFailures.add(1);
  }
  apiDuration.add(response.timings.duration);
}

function testAuthentication() {
  const loginPayload = JSON.stringify({
    username: 'testuser',
    password: 'testpass'
  });
  
  const response = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
    headers: { 'Content-Type': 'application/json' },
  });
  
  check(response, {
    '登录状态码为200': (r) => r.status === 200,
    '登录返回token': (r) => {
      try {
        const data = JSON.parse(r.body);
        return data.access_token !== undefined;
      } catch (e) {
        return false;
      }
    },
  });
  
  apiCalls.add(1);
  if (response.status !== 200) {
    apiFailures.add(1);
  }
  apiDuration.add(response.timings.duration);
}

// 压力测试配置
// tests/performance/stress-test.js
export const stressOptions = {
  stages: [
    { duration: '1m', target: 50 },   // 快速上升到50用户
    { duration: '1m', target: 100 },  // 上升到100用户
    { duration: '1m', target: 200 },  // 上升到200用户
    { duration: '1m', target: 300 },  // 上升到300用户
    { duration: '2m', target: 400 },  // 推到极限400用户
    { duration: '1m', target: 0 },    // 快速降级
  ],
  thresholds: {
    http_req_duration: ['p(90)<5000'], // 放宽要求
    http_req_failed: ['rate<0.1'],     // 允许10%失败率
  },
};

小结

自动化测试实战的核心要素:

🤖 自动化策略

  1. CI/CD集成 - 完整的流水线自动化
  2. 多层次测试 - 单元/集成/端到端/性能/安全测试
  3. 环境管理 - 开发/测试/暂存/生产环境隔离
  4. 质量门禁 - 代码质量、测试覆盖率、性能指标检查
  5. 快速反馈 - 及时发现问题并通知相关人员

💡 实践要点

  • 采用分层测试策略,底层测试快速反馈,顶层测试全面覆盖
  • 建立完善的Mock和测试数据管理机制
  • 实施渐进式部署,降低生产环境风险
  • 建立性能基准线和监控告警机制
  • 重视测试环境的稳定性和可维护性

🚀 自动化哲学:自动化测试不是万能的,但没有自动化测试是万万不能的。好的自动化测试让团队更有信心去创新和迭代。


下一节工作流设计模式 - 掌握常见的MCP应用架构模式