Skip to content

前端自动化测试实战

前端自动化测试是保证代码质量、提高开发效率的重要手段。本文将从测试策略到具体实现,全面介绍前端测试的最佳实践。

测试金字塔

前端测试通常遵循测试金字塔原则:

    /\
   /  \  E2E Tests (少量)
  /____\
 /      \
/________\ Integration Tests (适量)
/          \
/____________\ Unit Tests (大量)

1. 单元测试 (Unit Tests)

  • 目标: 测试独立的函数、组件
  • 特点: 快速、稳定、易维护
  • 占比: 70%

2. 集成测试 (Integration Tests)

  • 目标: 测试组件间的交互
  • 特点: 中等速度、中等复杂度
  • 占比: 20%

3. 端到端测试 (E2E Tests)

  • 目标: 测试完整的用户流程
  • 特点: 慢速、复杂、但最接近真实场景
  • 占比: 10%

测试工具生态

测试框架对比

框架优势劣势适用场景
Jest零配置、快照测试、内置断言主要针对 Node.jsReact、Vue、通用 JS
Vitest极快速度、与 Vite 集成相对较新Vite 项目
Mocha灵活、可配置性强需要额外配置复杂测试需求
Jasmine简单易用、BDD 风格功能相对基础简单项目

E2E 测试工具对比

工具优势劣势适用场景
Cypress开发体验好、调试方便只支持 Chrome 系现代 Web 应用
Playwright跨浏览器、性能好学习曲线陡峭企业级应用
PuppeteerGoogle 官方、功能强大只支持 ChromeChrome 专用场景

单元测试实战

1. Jest 基础配置

javascript
// jest.config.js
module.exports = {
  // 测试环境
  testEnvironment: 'jsdom',
  
  // 文件匹配模式
  testMatch: [
    '**/__tests__/**/*.(js|jsx|ts|tsx)',
    '**/*.(test|spec).(js|jsx|ts|tsx)'
  ],
  
  // 模块路径映射
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy'
  },
  
  // 设置文件
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  
  // 覆盖率配置
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js'
  ],
  
  // 转换配置
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
  }
};

2. 工具函数测试

javascript
// utils/math.js
export function add(a, b) {
  return a + b;
}

export function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

export function formatCurrency(amount) {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY'
  }).format(amount);
}
javascript
// utils/__tests__/math.test.js
import { add, divide, formatCurrency } from '../math';

describe('Math utilities', () => {
  describe('add', () => {
    test('should add two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });
    
    test('should handle negative numbers', () => {
      expect(add(-1, 1)).toBe(0);
      expect(add(-2, -3)).toBe(-5);
    });
    
    test('should handle decimal numbers', () => {
      expect(add(0.1, 0.2)).toBeCloseTo(0.3);
    });
  });
  
  describe('divide', () => {
    test('should divide two numbers', () => {
      expect(divide(10, 2)).toBe(5);
    });
    
    test('should throw error when dividing by zero', () => {
      expect(() => divide(10, 0)).toThrow('Division by zero');
    });
    
    test('should handle decimal results', () => {
      expect(divide(1, 3)).toBeCloseTo(0.333333);
    });
  });
  
  describe('formatCurrency', () => {
    test('should format positive amount', () => {
      expect(formatCurrency(1234.56)).toBe('¥1,234.56');
    });
    
    test('should format zero', () => {
      expect(formatCurrency(0)).toBe('¥0.00');
    });
    
    test('should format negative amount', () => {
      expect(formatCurrency(-100)).toBe('-¥100.00');
    });
  });
});

3. React 组件测试

jsx
// components/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';

const Button = ({ 
  children, 
  onClick, 
  disabled = false, 
  variant = 'primary',
  size = 'medium'
}) => {
  const className = `btn btn-${variant} btn-${size}`;
  
  return (
    <button 
      className={className}
      onClick={onClick}
      disabled={disabled}
      data-testid="button"
    >
      {children}
    </button>
  );
};

Button.propTypes = {
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func,
  disabled: PropTypes.bool,
  variant: PropTypes.oneOf(['primary', 'secondary', 'danger']),
  size: PropTypes.oneOf(['small', 'medium', 'large'])
};

export default Button;
jsx
// components/__tests__/Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from '../Button';

describe('Button Component', () => {
  test('renders button with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });
  
  test('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
  
  test('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
  
  test('applies correct CSS classes', () => {
    render(
      <Button variant="danger" size="large">
        Delete
      </Button>
    );
    
    const button = screen.getByRole('button');
    expect(button).toHaveClass('btn', 'btn-danger', 'btn-large');
  });
  
  test('does not call onClick when disabled', () => {
    const handleClick = jest.fn();
    render(
      <Button onClick={handleClick} disabled>
        Click me
      </Button>
    );
    
    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).not.toHaveBeenCalled();
  });
});

4. Vue 组件测试

vue
<!-- components/UserCard.vue -->
<template>
  <div class="user-card" data-testid="user-card">
    <img 
      :src="user.avatar" 
      :alt="`${user.name} avatar`"
      class="avatar"
    />
    <div class="user-info">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button 
        @click="$emit('follow', user.id)"
        :disabled="isFollowing"
        class="follow-btn"
      >
        {{ isFollowing ? 'Following' : 'Follow' }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    user: {
      type: Object,
      required: true
    },
    isFollowing: {
      type: Boolean,
      default: false
    }
  },
  emits: ['follow']
};
</script>
javascript
// components/__tests__/UserCard.test.js
import { mount } from '@vue/test-utils';
import UserCard from '../UserCard.vue';

describe('UserCard', () => {
  const mockUser = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    avatar: 'https://example.com/avatar.jpg'
  };
  
  test('renders user information', () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    });
    
    expect(wrapper.text()).toContain('John Doe');
    expect(wrapper.text()).toContain('john@example.com');
    expect(wrapper.find('img').attributes('src')).toBe(mockUser.avatar);
  });
  
  test('emits follow event when button clicked', async () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    });
    
    await wrapper.find('.follow-btn').trigger('click');
    
    expect(wrapper.emitted('follow')).toBeTruthy();
    expect(wrapper.emitted('follow')[0]).toEqual([mockUser.id]);
  });
  
  test('shows correct button text based on following status', () => {
    const wrapper = mount(UserCard, {
      props: { 
        user: mockUser,
        isFollowing: true
      }
    });
    
    expect(wrapper.find('.follow-btn').text()).toBe('Following');
    expect(wrapper.find('.follow-btn').attributes('disabled')).toBeDefined();
  });
});

集成测试实战

1. API 集成测试

javascript
// services/userService.js
class UserService {
  constructor(apiClient) {
    this.apiClient = apiClient;
  }
  
  async getUsers(page = 1, limit = 10) {
    const response = await this.apiClient.get('/users', {
      params: { page, limit }
    });
    return response.data;
  }
  
  async createUser(userData) {
    const response = await this.apiClient.post('/users', userData);
    return response.data;
  }
  
  async updateUser(id, userData) {
    const response = await this.apiClient.put(`/users/${id}`, userData);
    return response.data;
  }
}

export default UserService;
javascript
// services/__tests__/userService.test.js
import UserService from '../userService';

// Mock API client
const mockApiClient = {
  get: jest.fn(),
  post: jest.fn(),
  put: jest.fn()
};

describe('UserService', () => {
  let userService;
  
  beforeEach(() => {
    userService = new UserService(mockApiClient);
    jest.clearAllMocks();
  });
  
  describe('getUsers', () => {
    test('should fetch users with default pagination', async () => {
      const mockResponse = {
        data: {
          users: [{ id: 1, name: 'John' }],
          total: 1
        }
      };
      
      mockApiClient.get.mockResolvedValue(mockResponse);
      
      const result = await userService.getUsers();
      
      expect(mockApiClient.get).toHaveBeenCalledWith('/users', {
        params: { page: 1, limit: 10 }
      });
      expect(result).toEqual(mockResponse.data);
    });
    
    test('should handle API errors', async () => {
      const error = new Error('Network error');
      mockApiClient.get.mockRejectedValue(error);
      
      await expect(userService.getUsers()).rejects.toThrow('Network error');
    });
  });
  
  describe('createUser', () => {
    test('should create user successfully', async () => {
      const userData = { name: 'Jane', email: 'jane@example.com' };
      const mockResponse = { data: { id: 2, ...userData } };
      
      mockApiClient.post.mockResolvedValue(mockResponse);
      
      const result = await userService.createUser(userData);
      
      expect(mockApiClient.post).toHaveBeenCalledWith('/users', userData);
      expect(result).toEqual(mockResponse.data);
    });
  });
});

2. 状态管理测试

javascript
// store/userStore.js (Vuex)
export const state = () => ({
  users: [],
  loading: false,
  error: null
});

export const mutations = {
  SET_LOADING(state, loading) {
    state.loading = loading;
  },
  
  SET_USERS(state, users) {
    state.users = users;
  },
  
  SET_ERROR(state, error) {
    state.error = error;
  },
  
  ADD_USER(state, user) {
    state.users.push(user);
  }
};

export const actions = {
  async fetchUsers({ commit }, { page = 1, limit = 10 } = {}) {
    commit('SET_LOADING', true);
    commit('SET_ERROR', null);
    
    try {
      const response = await this.$api.get('/users', {
        params: { page, limit }
      });
      commit('SET_USERS', response.data.users);
    } catch (error) {
      commit('SET_ERROR', error.message);
    } finally {
      commit('SET_LOADING', false);
    }
  }
};
javascript
// store/__tests__/userStore.test.js
import { mutations, actions } from '../userStore';

describe('User Store', () => {
  describe('mutations', () => {
    test('SET_LOADING should update loading state', () => {
      const state = { loading: false };
      mutations.SET_LOADING(state, true);
      expect(state.loading).toBe(true);
    });
    
    test('SET_USERS should update users array', () => {
      const state = { users: [] };
      const users = [{ id: 1, name: 'John' }];
      mutations.SET_USERS(state, users);
      expect(state.users).toEqual(users);
    });
    
    test('ADD_USER should add user to array', () => {
      const state = { users: [] };
      const user = { id: 1, name: 'John' };
      mutations.ADD_USER(state, user);
      expect(state.users).toContain(user);
    });
  });
  
  describe('actions', () => {
    test('fetchUsers should handle successful API call', async () => {
      const commit = jest.fn();
      const mockApi = {
        get: jest.fn().mockResolvedValue({
          data: { users: [{ id: 1, name: 'John' }] }
        })
      };
      
      const context = { commit, $api: mockApi };
      
      await actions.fetchUsers(context);
      
      expect(commit).toHaveBeenCalledWith('SET_LOADING', true);
      expect(commit).toHaveBeenCalledWith('SET_USERS', [{ id: 1, name: 'John' }]);
      expect(commit).toHaveBeenCalledWith('SET_LOADING', false);
    });
    
    test('fetchUsers should handle API errors', async () => {
      const commit = jest.fn();
      const mockApi = {
        get: jest.fn().mockRejectedValue(new Error('API Error'))
      };
      
      const context = { commit, $api: mockApi };
      
      await actions.fetchUsers(context);
      
      expect(commit).toHaveBeenCalledWith('SET_ERROR', 'API Error');
      expect(commit).toHaveBeenCalledWith('SET_LOADING', false);
    });
  });
});

端到端测试实战

1. Cypress 基础配置

javascript
// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    
    setupNodeEvents(on, config) {
      // 插件配置
    },
    
    env: {
      apiUrl: 'http://localhost:8080/api'
    }
  },
  
  component: {
    devServer: {
      framework: 'react',
      bundler: 'webpack'
    }
  }
});

2. 用户登录流程测试

javascript
// cypress/e2e/auth.cy.js
describe('Authentication', () => {
  beforeEach(() => {
    cy.visit('/login');
  });
  
  it('should login with valid credentials', () => {
    // 输入用户名和密码
    cy.get('[data-testid="username"]').type('testuser');
    cy.get('[data-testid="password"]').type('password123');
    
    // 点击登录按钮
    cy.get('[data-testid="login-button"]').click();
    
    // 验证登录成功
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-menu"]').should('be.visible');
    cy.get('[data-testid="welcome-message"]')
      .should('contain', 'Welcome, testuser');
  });
  
  it('should show error for invalid credentials', () => {
    cy.get('[data-testid="username"]').type('invalid');
    cy.get('[data-testid="password"]').type('wrong');
    cy.get('[data-testid="login-button"]').click();
    
    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Invalid credentials');
    
    // 确保仍在登录页面
    cy.url().should('include', '/login');
  });
  
  it('should validate required fields', () => {
    cy.get('[data-testid="login-button"]').click();
    
    cy.get('[data-testid="username-error"]')
      .should('contain', 'Username is required');
    cy.get('[data-testid="password-error"]')
      .should('contain', 'Password is required');
  });
});

3. 购物车功能测试

javascript
// cypress/e2e/shopping-cart.cy.js
describe('Shopping Cart', () => {
  beforeEach(() => {
    // 登录用户
    cy.login('testuser', 'password123');
    cy.visit('/products');
  });
  
  it('should add product to cart', () => {
    // 选择第一个产品
    cy.get('[data-testid="product-card"]').first().within(() => {
      cy.get('[data-testid="product-name"]').invoke('text').as('productName');
      cy.get('[data-testid="add-to-cart"]').click();
    });
    
    // 验证添加成功提示
    cy.get('[data-testid="toast"]')
      .should('contain', 'Product added to cart');
    
    // 检查购物车图标更新
    cy.get('[data-testid="cart-count"]').should('contain', '1');
    
    // 打开购物车
    cy.get('[data-testid="cart-icon"]').click();
    
    // 验证产品在购物车中
    cy.get('@productName').then((productName) => {
      cy.get('[data-testid="cart-item"]')
        .should('contain', productName);
    });
  });
  
  it('should update quantity in cart', () => {
    // 添加产品到购物车
    cy.get('[data-testid="product-card"]').first().within(() => {
      cy.get('[data-testid="add-to-cart"]').click();
    });
    
    // 打开购物车
    cy.get('[data-testid="cart-icon"]').click();
    
    // 增加数量
    cy.get('[data-testid="quantity-increase"]').click();
    cy.get('[data-testid="quantity-input"]').should('have.value', '2');
    
    // 减少数量
    cy.get('[data-testid="quantity-decrease"]').click();
    cy.get('[data-testid="quantity-input"]').should('have.value', '1');
  });
  
  it('should complete checkout process', () => {
    // 添加产品并进入结账
    cy.addProductToCart();
    cy.get('[data-testid="checkout-button"]').click();
    
    // 填写配送信息
    cy.get('[data-testid="shipping-address"]').type('123 Test Street');
    cy.get('[data-testid="shipping-city"]').type('Test City');
    cy.get('[data-testid="shipping-zip"]').type('12345');
    
    // 选择支付方式
    cy.get('[data-testid="payment-method"]').select('credit-card');
    cy.get('[data-testid="card-number"]').type('4111111111111111');
    cy.get('[data-testid="card-expiry"]').type('12/25');
    cy.get('[data-testid="card-cvv"]').type('123');
    
    // 提交订单
    cy.get('[data-testid="place-order"]').click();
    
    // 验证订单成功
    cy.url().should('include', '/order-confirmation');
    cy.get('[data-testid="order-success"]')
      .should('contain', 'Order placed successfully');
  });
});

4. 自定义命令

javascript
// cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
  cy.session([username, password], () => {
    cy.visit('/login');
    cy.get('[data-testid="username"]').type(username);
    cy.get('[data-testid="password"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
  });
});

Cypress.Commands.add('addProductToCart', (productIndex = 0) => {
  cy.visit('/products');
  cy.get('[data-testid="product-card"]').eq(productIndex).within(() => {
    cy.get('[data-testid="add-to-cart"]').click();
  });
  cy.get('[data-testid="toast"]').should('contain', 'Product added to cart');
});

Cypress.Commands.add('clearCart', () => {
  cy.get('[data-testid="cart-icon"]').click();
  cy.get('[data-testid="clear-cart"]').click();
  cy.get('[data-testid="confirm-clear"]').click();
});

测试最佳实践

1. 测试组织

tests/
├── unit/
│   ├── components/
│   ├── utils/
│   └── services/
├── integration/
│   ├── api/
│   └── store/
├── e2e/
│   ├── auth/
│   ├── shopping/
│   └── admin/
└── fixtures/
    ├── users.json
    └── products.json

2. 测试数据管理

javascript
// tests/fixtures/users.js
export const mockUsers = [
  {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    role: 'user'
  },
  {
    id: 2,
    name: 'Jane Admin',
    email: 'jane@example.com',
    role: 'admin'
  }
];

export const createMockUser = (overrides = {}) => ({
  id: Math.random(),
  name: 'Test User',
  email: 'test@example.com',
  role: 'user',
  ...overrides
});

3. 测试工具函数

javascript
// tests/utils/testUtils.js
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { createStore } from 'redux';

// 自定义渲染函数
export function renderWithProviders(
  ui,
  {
    preloadedState = {},
    store = createStore(() => preloadedState),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return (
      <Provider store={store}>
        <BrowserRouter>
          {children}
        </BrowserRouter>
      </Provider>
    );
  }
  
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

// 等待异步操作
export const waitForLoadingToFinish = () =>
  waitFor(() => {
    expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
  });

// 模拟用户交互
export const userEvent = {
  type: async (element, text) => {
    await fireEvent.change(element, { target: { value: text } });
  },
  
  click: async (element) => {
    await fireEvent.click(element);
  },
  
  selectOption: async (select, option) => {
    await fireEvent.change(select, { target: { value: option } });
  }
};

4. 持续集成配置

yaml
# .github/workflows/test.yml
name: Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16.x, 18.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
    
    - name: Run integration tests
      run: npm run test:integration
    
    - name: Start application
      run: npm start &
      
    - name: Wait for application
      run: npx wait-on http://localhost:3000
    
    - name: Run E2E tests
      run: npm run test:e2e
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

性能测试

1. 加载性能测试

javascript
// tests/performance/lighthouse.test.js
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

describe('Performance Tests', () => {
  let chrome;
  
  beforeAll(async () => {
    chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
  });
  
  afterAll(async () => {
    await chrome.kill();
  });
  
  test('homepage should meet performance standards', async () => {
    const options = {
      logLevel: 'info',
      output: 'json',
      onlyCategories: ['performance'],
      port: chrome.port
    };
    
    const runnerResult = await lighthouse('http://localhost:3000', options);
    const { lhr } = runnerResult;
    
    expect(lhr.categories.performance.score).toBeGreaterThan(0.9);
    expect(lhr.audits['first-contentful-paint'].numericValue).toBeLessThan(2000);
    expect(lhr.audits['largest-contentful-paint'].numericValue).toBeLessThan(4000);
  });
});

2. 内存泄漏测试

javascript
// tests/performance/memory.test.js
describe('Memory Leak Tests', () => {
  test('should not leak memory on route changes', async () => {
    const initialMemory = performance.memory.usedJSHeapSize;
    
    // 模拟多次路由切换
    for (let i = 0; i < 10; i++) {
      await navigateToRoute('/products');
      await navigateToRoute('/cart');
      await navigateToRoute('/profile');
    }
    
    // 强制垃圾回收
    if (global.gc) {
      global.gc();
    }
    
    const finalMemory = performance.memory.usedJSHeapSize;
    const memoryIncrease = finalMemory - initialMemory;
    
    // 内存增长不应超过 10MB
    expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024);
  });
});

测试覆盖率

1. 覆盖率配置

javascript
// jest.config.js
module.exports = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/serviceWorker.js',
    '!src/**/*.stories.{js,jsx,ts,tsx}'
  ],
  
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    },
    './src/components/': {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90
    }
  }
};

2. 覆盖率报告分析

bash
# 生成覆盖率报告
npm run test -- --coverage

# 查看详细报告
open coverage/lcov-report/index.html

覆盖率指标说明:

  • 语句覆盖率 (Statements): 执行的语句占总语句的比例
  • 分支覆盖率 (Branches): 执行的分支占总分支的比例
  • 函数覆盖率 (Functions): 调用的函数占总函数的比例
  • 行覆盖率 (Lines): 执行的代码行占总行数的比例

测试调试技巧

1. Jest 调试

javascript
// 使用 console.log 调试
test('debug example', () => {
  const result = someFunction();
  console.log('Result:', result); // 会在测试输出中显示
  expect(result).toBe(expected);
});

// 使用 debugger
test('debugger example', () => {
  const result = someFunction();
  debugger; // 在 Node.js 调试模式下会暂停
  expect(result).toBe(expected);
});

// 使用 screen.debug() 查看 DOM
test('DOM debug example', () => {
  render(<MyComponent />);
  screen.debug(); // 打印当前 DOM 结构
});

2. Cypress 调试

javascript
// 使用 cy.debug() 暂停执行
cy.get('[data-testid="button"]').debug().click();

// 使用 cy.pause() 暂停测试
cy.pause();

// 在浏览器中查看元素
cy.get('[data-testid="element"]').then(($el) => {
  debugger; // 在浏览器开发者工具中暂停
});

测试策略制定

1. 测试计划模板

markdown
# 测试计划

## 项目概述
- 项目名称: 
- 测试范围: 
- 测试目标: 

## 测试策略
- 单元测试覆盖率目标: 80%
- 集成测试重点: API 集成、状态管理
- E2E 测试场景: 关键用户流程

## 测试环境
- 开发环境: localhost:3000
- 测试环境: test.example.com
- 生产环境: example.com

## 测试工具
- 单元测试: Jest + Testing Library
- E2E 测试: Cypress
- 性能测试: Lighthouse

## 测试数据
- 测试用户账号
- 模拟数据集
- API Mock 数据

## 风险评估
- 高风险功能: 支付流程、用户认证
- 中风险功能: 数据展示、表单提交
- 低风险功能: 静态页面、样式调整

2. 测试用例设计

javascript
// 测试用例模板
describe('Feature: User Registration', () => {
  describe('Scenario: Valid registration', () => {
    test('Given valid user data, When user submits form, Then account is created', () => {
      // Arrange (准备)
      const userData = {
        username: 'testuser',
        email: 'test@example.com',
        password: 'SecurePass123!'
      };
      
      // Act (执行)
      const result = registerUser(userData);
      
      // Assert (断言)
      expect(result.success).toBe(true);
      expect(result.user.id).toBeDefined();
    });
  });
  
  describe('Scenario: Invalid email format', () => {
    test('Given invalid email, When user submits form, Then validation error is shown', () => {
      const userData = {
        username: 'testuser',
        email: 'invalid-email',
        password: 'SecurePass123!'
      };
      
      expect(() => registerUser(userData))
        .toThrow('Invalid email format');
    });
  });
});

常见问题解决

1. 异步测试问题

javascript
// ❌ 错误:没有等待异步操作
test('async test - wrong', () => {
  fetchData().then(data => {
    expect(data).toBeDefined();
  });
});

// ✅ 正确:使用 async/await
test('async test - correct', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});

// ✅ 正确:返回 Promise
test('async test - promise', () => {
  return fetchData().then(data => {
    expect(data).toBeDefined();
  });
});

2. 时间相关测试

javascript
// 使用 Jest 的时间模拟
beforeEach(() => {
  jest.useFakeTimers();
});

afterEach(() => {
  jest.useRealTimers();
});

test('should call callback after timeout', () => {
  const callback = jest.fn();
  
  setTimeout(callback, 1000);
  
  // 快进时间
  jest.advanceTimersByTime(1000);
  
  expect(callback).toHaveBeenCalled();
});

3. 网络请求模拟

javascript
// 使用 MSW (Mock Service Worker)
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' }
      ])
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

测试自动化

1. Git Hooks

bash
# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run test:unit
npm run lint

2. 测试脚本

json
{
  "scripts": {
    "test": "jest",
    "test:unit": "jest --testPathPattern=unit",
    "test:integration": "jest --testPathPattern=integration",
    "test:e2e": "cypress run",
    "test:e2e:open": "cypress open",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch",
    "test:ci": "jest --ci --coverage --watchAll=false"
  }
}

总结

前端自动化测试是保证代码质量的重要手段。关键要点:

  1. 遵循测试金字塔:大量单元测试,适量集成测试,少量 E2E 测试
  2. 选择合适工具:根据项目需求选择测试框架和工具
  3. 编写可维护的测试:清晰的测试结构,合理的测试数据管理
  4. 持续改进:定期审查测试覆盖率,优化测试策略

通过系统性的测试实践,可以显著提高代码质量,减少生产环境问题,提升开发效率。

参考资源

vitepress开发指南