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开发指南