前端测试实践记录
一、测试框架概述
1. Jest
英文全称:Jest 中文解释:Facebook开源的JavaScript测试框架,专注于简洁易用
核心特点:
- 零配置,开箱即用
- 内置断言库、测试覆盖率工具
- 快照测试功能
- Mock功能强大
- 支持并行测试
基本使用:
// 安装
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兼容
- 热更新支持
- 轻量级
基本使用:
// 安装
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测试框架
核心特点:
- 高度可配置
- 支持多种断言库
- 异步测试支持
- 丰富的报告生成器
基本使用:
// 安装
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)
- 自动等待功能
- 网络请求拦截
- 移动设备模拟
- 并行测试
基本使用:
// 安装
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测试框架
核心特点:
- 实时重新加载
- 自动等待
- 可视化测试运行器
- 网络请求控制
- 时间旅行调试
基本使用:
// 安装
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%左右)
示例:
// 测试工具函数
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):
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):
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调用
- 状态管理
- 路由
最佳实践:
- 模拟外部依赖
- 测试关键业务流程
- 避免过度测试
示例:
// 测试组件与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):
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断言
基本断言:
// 相等性断言
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查询
常用查询方法:
// 按文本查找
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断言:
// 页面断言
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断言:
// 页面断言
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:
// 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:
// 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:
// 拦截网络请求
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:
// 拦截网络请求
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示例:
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.info3. 测试覆盖率
覆盖率指标:
- 行覆盖率:执行了多少行代码
- 分支覆盖率:执行了多少条件分支
- 函数覆盖率:执行了多少函数
- 语句覆盖率:执行了多少语句
配置示例:
// 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. 异步测试问题
问题:测试在异步操作完成前就结束了
解决方案:
// 使用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. 测试顺序依赖
问题:测试之间相互依赖,导致不稳定
解决方案:
// 每个测试独立运行
test('test 1', () => {
// 独立测试
});
test('test 2', () => {
// 独立测试
});
// 使用beforeEach重置状态
beforeEach(() => {
// 重置测试环境
});3. 过度测试实现细节
问题:测试组件的内部实现而非外部行为
解决方案:
// 不好的做法:测试内部状态
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太多导致测试失去实际意义
解决方案:
// 适度Mock,只Mock外部依赖
jest.mock('./api'); // Mock外部API
// 不Mock内部组件或函数5. 测试速度慢
问题:测试套件运行时间过长
解决方案:
// 使用并行测试
// jest.config.js
module.exports = {
maxWorkers: '50%' // 使用50%的CPU核心
};
// 避免不必要的渲染
test('test', () => {
// 只渲染必要的组件
render(<Component />);
});
// 清理测试环境
afterEach(() => {
// 清理DOM、重置状态等
});6. E2E测试不稳定
问题:E2E测试有时通过有时失败
解决方案:
// 使用自动等待
// 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. 快速上手步骤
安装框架:
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编写第一个测试:
javascript// 单元测试 test('adds 1 + 2 to equal 3', () => { expect(1 + 2).toBe(3); });运行测试:
bashnpm run test集成到CI/CD:
- 配置GitHub Actions/GitLab CI
- 添加测试步骤
- 上传覆盖率报告
3. 企业级最佳实践
- 测试分层:遵循金字塔模型
- 测试命名:清晰描述测试内容
- 测试隔离:每个测试独立运行
- 模拟外部依赖:避免网络请求等不可控因素
- 定期运行:在CI/CD中自动运行
- 覆盖率监控:设置合理的覆盖率目标
- 测试评审:与代码评审一起进行
通过本文的学习,你应该能够快速掌握前端测试的核心概念、常用框架和实践技巧,为企业级项目提供可靠的质量保障。