Skip to content

前端测试实践记录

一、测试框架概述

1. Jest

英文全称:Jest 中文解释:Facebook开源的JavaScript测试框架,专注于简洁易用

核心特点

  • 零配置,开箱即用
  • 内置断言库、测试覆盖率工具
  • 快照测试功能
  • Mock功能强大
  • 支持并行测试

基本使用

javascript
// 安装
npm install --save-dev jest

// 配置package.json
{
  "scripts": {
    "test": "jest"
  }
}

// 编写测试
function sum(a, b) {
  return a + b;
}

test('sum adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

常用API

  • test(name, fn, timeout):定义测试用例
  • expect(value):断言
  • toBe(value):精确匹配
  • toEqual(value):深度匹配
  • toBeTruthy()/toBeFalsy():布尔值匹配
  • toBeNull()/toBeUndefined():空值匹配
  • toThrow():异常匹配
  • beforeEach()/afterEach():测试前后执行
  • beforeAll()/afterAll():所有测试前后执行

企业级应用

  • 单元测试、集成测试
  • React、Vue等框架的组件测试
  • Node.js后端测试
  • 大型项目的测试自动化

2. Vitest

英文全称:Vitest 中文解释:基于Vite的现代化测试框架

核心特点

  • 与Vite集成,速度极快
  • 支持ESM、TypeScript
  • 与Jest API兼容
  • 热更新支持
  • 轻量级

基本使用

javascript
// 安装
npm install --save-dev vitest

// 配置vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    globals: true, // 启用全局测试API(无需导入describe/it/expect)
    environment: 'jsdom', // 使用jsdom环境模拟浏览器
    setupFiles: './setupTests.js', // 测试前执行的文件
    coverage: {
      provider: 'c8', // 使用c8作为覆盖率报告生成器
      reporter: ['text', 'json', 'html'], // 覆盖率报告格式
      include: ['src/**/*'], // 包含的文件
      exclude: ['node_modules', '**/*.test.js'] // 排除的文件
    },
    mockReset: true, // 每次测试后重置所有mock
    clearMocks: true, // 每次测试前清除所有mock
    testTimeout: 5000, // 测试超时时间(毫秒)
    server: {
      deps: {
        inline: ['vitest-canvas-mock'] // 需要内联的依赖
      }
    }
  }
})

// 编写测试
import { describe, it, expect } from 'vitest'

describe('sum', () => {
  it('adds 1 + 2 to equal 3', () => {
    expect(1 + 2).toBe(3)
  })
})

常用API

  • 与Jest基本兼容
  • describe():测试套件
  • it()/test():测试用例
  • expect():断言
  • vi.mock():Mock功能
  • vi.spyOn():函数监控

企业级应用

  • Vite项目的首选测试框架
  • 快速迭代的现代前端项目
  • 与Vue 3、React 18等新框架完美配合

3. Mocha

英文全称:Mocha 中文解释:灵活的JavaScript测试框架

核心特点

  • 高度可配置
  • 支持多种断言库
  • 异步测试支持
  • 丰富的报告生成器

基本使用

javascript
// 安装
npm install --save-dev mocha chai

// 配置package.json
{
  "scripts": {
    "test": "mocha"
  }
}

// 编写测试
const assert = require('chai').assert;

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal([1,2,3].indexOf(4), -1);
    });
  });
});

常用API

  • describe():测试套件
  • it():测试用例
  • before()/after():测试套件前后
  • beforeEach()/afterEach():测试用例前后

企业级应用

  • 需要高度定制化的测试场景
  • 与各种断言库(Chai、Should.js等)配合使用
  • 旧项目的维护

4. Playwright

英文全称:Playwright 中文解释:Microsoft开源的E2E测试框架

核心特点

  • 跨浏览器支持(Chrome、Firefox、Safari)
  • 自动等待功能
  • 网络请求拦截
  • 移动设备模拟
  • 并行测试

基本使用

javascript
// 安装
npm install --save-dev playwright

// 初始化配置
npx playwright init

// 编写测试
const { test, expect } = require('@playwright/test');

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

常用API

  • page.goto():导航到页面
  • page.click():点击元素
  • page.fill():填充表单
  • page.waitForSelector():等待元素
  • page.screenshot():截图

企业级应用

  • 跨浏览器的E2E测试
  • 复杂用户流程测试
  • 视觉回归测试
  • CI/CD集成

5. Cypress

英文全称:Cypress 中文解释:现代化的前端E2E测试框架

核心特点

  • 实时重新加载
  • 自动等待
  • 可视化测试运行器
  • 网络请求控制
  • 时间旅行调试

基本使用

javascript
// 安装
npm install --save-dev cypress

// 配置package.json
{
  "scripts": {
    "cypress:open": "cypress open"
  }
}

// 编写测试
describe('My First Test', () => {
  it('Visits the Kitchen Sink', () => {
    cy.visit('https://example.cypress.io');
    cy.contains('type').click();
    cy.url().should('include', '/commands/actions');
    cy.get('.action-email').type('fake@email.com').should('have.value', 'fake@email.com');
  });
});

常用API

  • cy.visit():访问页面
  • cy.get():获取元素
  • cy.click():点击
  • cy.type():输入
  • cy.should():断言

企业级应用

  • 前端应用的E2E测试
  • 团队协作的测试平台
  • 可视化测试报告

二、测试类型详解

1. 单元测试 (Unit Testing)

英文全称:Unit Testing 中文解释:测试最小可测试单元(函数、组件等)

测试对象

  • 独立函数
  • 组件内部逻辑
  • 工具类
  • 数据处理函数

最佳实践

  • 测试应该是独立的
  • 只测试单一功能点
  • 避免测试实现细节
  • 覆盖率目标合理(80%左右)

示例

javascript
// 测试工具函数
function formatPrice(price) {
  return `¥${price.toFixed(2)}`;
}

test('formatPrice formats price correctly', () => {
  expect(formatPrice(10)).toBe('¥10.00');
  expect(formatPrice(10.5)).toBe('¥10.50');
});

2. 组件测试 (Component Testing)

英文全称:Component Testing 中文解释:测试UI组件的行为和渲染

测试对象

  • Vue组件
  • React组件
  • Svelte组件等

核心工具

  • Vue:@vue/test-utils
  • React:React Testing Library、Enzyme
  • Svelte:@testing-library/svelte

示例(React)

javascript
import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders button with correct text', () => {
  render(<Button>Click me</Button>);
  const buttonElement = screen.getByText(/click me/i);
  expect(buttonElement).toBeInTheDocument();
});

示例(Vue)

javascript
import { mount } from '@vue/test-utils';
import Button from './Button.vue';

test('renders button with correct text', () => {
  const wrapper = mount(Button, {
    props: {
      label: 'Click me'
    }
  });
  expect(wrapper.text()).toContain('Click me');
});

3. 集成测试 (Integration Testing)

英文全称:Integration Testing 中文解释:测试多个组件或模块之间的交互

测试对象

  • 组件间的数据流
  • API调用
  • 状态管理
  • 路由

最佳实践

  • 模拟外部依赖
  • 测试关键业务流程
  • 避免过度测试

示例

javascript
// 测试组件与API的集成
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
import { fetchUser } from './api';

// Mock API
jest.mock('./api', () => ({
  fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'John' }))
}));

test('renders user profile after fetching data', async () => {
  render(<UserProfile userId={1} />);
  
  // 等待API调用完成
  await waitFor(() => {
    expect(screen.getByText('John')).toBeInTheDocument();
  });
});

4. E2E测试 (End-to-End Testing)

英文全称:End-to-End Testing 中文解释:模拟真实用户行为的完整流程测试

测试对象

  • 用户注册流程
  • 购物车功能
  • 支付流程
  • 完整业务场景

核心工具

  • Playwright
  • Cypress
  • Selenium

示例(Playwright)

javascript
const { test, expect } = require('@playwright/test');

test('user can login successfully', async ({ page }) => {
  // 访问登录页
  await page.goto('https://example.com/login');
  
  // 输入用户名密码
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  
  // 点击登录按钮
  await page.click('#login-button');
  
  // 验证登录成功
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page).toHaveTitle(/Dashboard/);
});

三、常用断言与匹配器

1. Jest/Vitest断言

基本断言

javascript
// 相等性断言
expect(1 + 2).toBe(3); // 严格相等
expect({ a: 1 }).toEqual({ a: 1 }); // 深度相等

// 真值断言
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();

// 数字断言
expect(5).toBeGreaterThan(3);
expect(5).toBeLessThan(10);
expect(5).toBeGreaterThanOrEqual(5);
expect(5).toBeLessThanOrEqual(5);
expect(0.1 + 0.2).toBeCloseTo(0.3); // 浮点数比较

// 字符串断言
expect('hello').toMatch(/ell/);
expect('hello').toContain('ell');

// 数组断言
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);

// 异常断言
expect(() => { throw new Error('test'); }).toThrow();
expect(() => { throw new Error('test'); }).toThrow('test');

2. React Testing Library查询

常用查询方法

javascript
// 按文本查找
screen.getByText('Submit'); // 精确匹配
screen.getByText(/submit/i); // 正则匹配
screen.getByDisplayValue('hello'); // 表单值

// 按角色查找
screen.getByRole('button'); // 按钮
screen.getByRole('textbox'); // 文本框
screen.getByRole('listitem'); // 列表项

// 按标签查找
screen.getByLabelText('Username'); // 标签文本

// 按占位符查找
screen.getByPlaceholderText('Enter username'); // 占位符

// 按Alt文本查找
screen.getByAltText('Profile picture'); // 图片Alt

// 异步查找
screen.findByText('Loading...'); // 等待元素出现

3. Playwright/Cypress断言

Playwright断言

javascript
// 页面断言
await expect(page).toHaveTitle(/Example/);
await expect(page).toHaveURL('https://example.com');

// 元素断言
await expect(page.locator('button')).toBeVisible();
await expect(page.locator('input')).toHaveValue('test');
await expect(page.locator('.error')).toHaveText('Invalid input');

// 计数断言
await expect(page.locator('li')).toHaveCount(5);

Cypress断言

javascript
// 页面断言
cy.url().should('include', '/dashboard');
cy.title().should('eq', 'Dashboard');

// 元素断言
cy.get('button').should('be.visible');
cy.get('input').should('have.value', 'test');
cy.get('.error').should('contain', 'Invalid input');

// 计数断言
cy.get('li').should('have.length', 5);

四、Mock与Stub

1. Jest/Vitest Mock

基本Mock

javascript
// Mock函数
const mockFunction = jest.fn();
mockFunction('test');
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).toHaveBeenCalledWith('test');

// Mock模块
jest.mock('./api', () => ({
  fetchData: jest.fn(() => Promise.resolve('mocked data'))
}));

// Mock返回值
const mockFunction = jest.fn();
mockFunction.mockReturnValue('default');
mockFunction.mockReturnValueOnce('first call');
mockFunction.mockReturnValueOnce('second call');

expect(mockFunction()).toBe('first call');
expect(mockFunction()).toBe('second call');
expect(mockFunction()).toBe('default');

2. 网络请求Mock

Jest/Vitest

javascript
// Mock fetch
global.fetch = jest.fn(() => 
  Promise.resolve({
    json: () => Promise.resolve({ data: 'test' })
  })
);

// 测试
async function fetchData() {
  const response = await fetch('https://example.com');
  return response.json();
}

test('fetchData returns data', async () => {
  const data = await fetchData();
  expect(data).toEqual({ data: 'test' });
});

Playwright

javascript
// 拦截网络请求
await page.route('https://api.example.com/data', async route => {
  const json = { data: 'mocked' };
  await route.fulfill({
    status: 200,
    content-type: 'application/json',
    body: JSON.stringify(json)
  });
});

// 访问页面
await page.goto('https://example.com');

Cypress

javascript
// 拦截网络请求
cy.intercept('GET', 'https://api.example.com/data', {
  statusCode: 200,
  body: { data: 'mocked' }
}).as('getData');

// 访问页面
cy.visit('https://example.com');
cy.wait('@getData');

五、企业级测试实践

1. 测试分层策略

金字塔模型

  • 单元测试(底部):最多,测试单个组件/函数
  • 集成测试(中间):适中,测试组件间交互
  • E2E测试(顶部):最少,测试完整业务流程

比例建议

  • 单元测试:70%
  • 集成测试:20%
  • E2E测试:10%

2. CI/CD集成

GitHub Actions示例

yaml
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm install
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

3. 测试覆盖率

覆盖率指标

  • 行覆盖率:执行了多少行代码
  • 分支覆盖率:执行了多少条件分支
  • 函数覆盖率:执行了多少函数
  • 语句覆盖率:执行了多少语句

配置示例

javascript
// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/index.js',
    '!src/**/*.d.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
}

六、常见易错点与解决方案

1. 异步测试问题

问题:测试在异步操作完成前就结束了

解决方案

javascript
// 使用async/await
test('async test', async () => {
  const result = await fetchData();
  expect(result).toBe('data');
});

// 使用done回调
test('async test with done', (done) => {
  fetchData().then(result => {
    expect(result).toBe('data');
    done();
  });
});

// 使用resolves/rejects
test('async test with resolves', () => {
  return expect(fetchData()).resolves.toBe('data');
});

2. 测试顺序依赖

问题:测试之间相互依赖,导致不稳定

解决方案

javascript
// 每个测试独立运行
test('test 1', () => {
  // 独立测试
});

test('test 2', () => {
  // 独立测试
});

// 使用beforeEach重置状态
beforeEach(() => {
  // 重置测试环境
});

3. 过度测试实现细节

问题:测试组件的内部实现而非外部行为

解决方案

javascript
// 不好的做法:测试内部状态
test('component updates internal state', () => {
  const wrapper = mount(Button);
  wrapper.vm.internalState = 'clicked';
  expect(wrapper.vm.internalState).toBe('clicked');
});

// 好的做法:测试用户可见的行为
test('button click triggers action', () => {
  const onClick = jest.fn();
  const wrapper = mount(<Button onClick={onClick} />);
  wrapper.find('button').trigger('click');
  expect(onClick).toHaveBeenCalled();
});

4. Mock过度使用

问题:Mock太多导致测试失去实际意义

解决方案

javascript
// 适度Mock,只Mock外部依赖
jest.mock('./api'); // Mock外部API
// 不Mock内部组件或函数

5. 测试速度慢

问题:测试套件运行时间过长

解决方案

javascript
// 使用并行测试
// jest.config.js
module.exports = {
  maxWorkers: '50%' // 使用50%的CPU核心
};

// 避免不必要的渲染
test('test', () => {
  // 只渲染必要的组件
  render(<Component />);
});

// 清理测试环境
afterEach(() => {
  // 清理DOM、重置状态等
});

6. E2E测试不稳定

问题:E2E测试有时通过有时失败

解决方案

javascript
// 使用自动等待
// Playwright自动等待
await page.click('#button'); // 自动等待按钮可点击

// 避免硬编码等待
// 不好的做法
await page.waitForTimeout(1000);

// 好的做法
await page.waitForSelector('#element');

// 稳定的选择器
// 不好的做法:使用CSS类名
page.locator('.btn-primary');

// 好的做法:使用ARIA角色
page.locator('[role="button"]');

七、总结与快速入门指南

1. 快速选择测试框架

测试类型推荐框架特点
单元测试Jest/Vitest配置简单、功能全面
组件测试Jest + @vue/test-utils/React Testing Library贴近用户行为
E2E测试Playwright/Cypress现代化、易用性高

2. 快速上手步骤

  1. 安装框架

    bash
    # Jest
    npm install --save-dev jest
    
    # Vitest
    npm install --save-dev vitest
    
    # Playwright
    npm install --save-dev playwright
    npx playwright install
    
    # Cypress
    npm install --save-dev cypress
  2. 编写第一个测试

    javascript
    // 单元测试
    test('adds 1 + 2 to equal 3', () => {
      expect(1 + 2).toBe(3);
    });
  3. 运行测试

    bash
    npm run test
  4. 集成到CI/CD

    • 配置GitHub Actions/GitLab CI
    • 添加测试步骤
    • 上传覆盖率报告

3. 企业级最佳实践

  • 测试分层:遵循金字塔模型
  • 测试命名:清晰描述测试内容
  • 测试隔离:每个测试独立运行
  • 模拟外部依赖:避免网络请求等不可控因素
  • 定期运行:在CI/CD中自动运行
  • 覆盖率监控:设置合理的覆盖率目标
  • 测试评审:与代码评审一起进行

通过本文的学习,你应该能够快速掌握前端测试的核心概念、常用框架和实践技巧,为企业级项目提供可靠的质量保障。

Updated at:

Released under the MIT License.