Skip to content

前端测试实践:Jest 与 Playwright 深度指南 ​

一、前端测试概述 ​

前端测试是保障Web应用质量的关键环节,随着前端工程化的发展,测试已经成为企业级开发流程中不可或缺的一部分。本文将深入讲解当今最流行的两大测试框架:Jest(单元/组件测试)和Playwright(E2E测试),帮助你掌握企业级测试的核心方法和最佳实践。

二、Jest 深度详解 ​

1. 核心概念与优势 ​

Jest 是 Facebook 开源的 JavaScript 测试框架,以其零配置、开箱即用的特点成为前端单元测试和组件测试的首选。

核心优势:

  • 内置断言库、测试覆盖率工具
  • 强大的 Mock 系统
  • 快照测试功能
  • 并行测试支持
  • 丰富的生态系统(支持 React、Vue、Angular 等)
  • 优秀的错误提示

2. 企业级配置 ​

在企业项目中,Jest 需要更精细的配置以满足复杂需求:

javascript
// jest.config.js
module.exports = {
  // 测试环境
  testEnvironment: 'jsdom', // 浏览器环境
  // testEnvironment: 'node', // Node.js环境
  
  // 文件匹配规则
  testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
  
  // 模块解析
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1', // 别名映射
    '\.(css|less|scss|sass)$': 'identity-obj-proxy', // 样式文件Mock
  },
  
  // 覆盖率配置
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/index.js',
    '!src/**/*.d.ts',
    '!src/**/mock/*'
  ],
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'clover'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  
  // 转换配置
  transform: {
    '^.+\.(js|jsx|ts|tsx)$': ['babel-jest', {
      presets: ['@babel/preset-env', '@babel/preset-react']
    }],
    '^.+\.tsx?$': 'ts-jest'
  },
  
  // 测试前后钩子
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
  
  // 并行测试配置
  maxWorkers: process.env.CI ? 2 : '50%',
  
  // 清除Mock
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true
};

3. 经典案例:企业级应用场景 ​

案例1:工具函数单元测试 ​

javascript
// 源文件:src/utils/format.js
export function formatDate(date, format = 'YYYY-MM-DD') {
  // 实现逻辑...
  return formattedDate;
}

export function calculateDiscount(price, discountRate) {
  if (typeof price !== 'number' || typeof discountRate !== 'number') {
    throw new Error('参数必须为数字');
  }
  if (discountRate < 0 || discountRate > 1) {
    throw new Error('折扣率必须在0-1之间');
  }
  return price * (1 - discountRate);
}

// 测试文件:src/utils/__tests__/format.test.js
import { formatDate, calculateDiscount } from '../format';

describe('formatDate', () => {
  it('should format date correctly with default format', () => {
    const date = new Date('2023-10-01T12:00:00');
    expect(formatDate(date)).toBe('2023-10-01');
  });
  
  it('should format date with custom format', () => {
    const date = new Date('2023-10-01T14:30:00');
    expect(formatDate(date, 'YYYY/MM/DD HH:mm')).toBe('2023/10/01 14:30');
  });
});

describe('calculateDiscount', () => {
  it('should calculate discount correctly', () => {
    expect(calculateDiscount(100, 0.2)).toBe(80);
    expect(calculateDiscount(200, 0.5)).toBe(100);
  });
  
  it('should throw error when parameters are not numbers', () => {
    expect(() => calculateDiscount('100', 0.2)).toThrow('参数必须为数字');
    expect(() => calculateDiscount(100, '0.2')).toThrow('参数必须为数字');
  });
  
  it('should throw error when discountRate is out of range', () => {
    expect(() => calculateDiscount(100, -0.1)).toThrow('折扣率必须在0-1之间');
    expect(() => calculateDiscount(100, 1.1)).toThrow('折扣率必须在0-1之间');
  });
});

案例2:React 组件测试(使用 React Testing Library) ​

javascript
// 源文件:src/components/LoginForm.jsx
import React, { useState } from 'react';

const LoginForm = ({ onSubmit }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (!email || !password) {
      setError('请输入邮箱和密码');
      return;
    }
    
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      setError('请输入有效的邮箱地址');
      return;
    }
    
    onSubmit({ email, password });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>登录</h2>
      {error && <div className="error">{error}</div>}
      <div>
        <label htmlFor="email">邮箱</label>
        <input 
          type="email" 
          id="email" 
          value={email} 
          onChange={(e) => setEmail(e.target.value)}
          placeholder="请输入邮箱"
        />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input 
          type="password" 
          id="password" 
          value={password} 
          onChange={(e) => setPassword(e.target.value)}
          placeholder="请输入密码"
        />
      </div>
      <button type="submit">登录</button>
    </form>
  );
};

export default LoginForm;

// 测试文件:src/components/__tests__/LoginForm.test.jsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import LoginForm from '../LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn();
  
  beforeEach(() => {
    mockOnSubmit.mockClear();
  });
  
  it('should render login form correctly', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);
    
    expect(screen.getByText('登录')).toBeInTheDocument();
    expect(screen.getByLabelText('邮箱')).toBeInTheDocument();
    expect(screen.getByLabelText('密码')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: '登录' })).toBeInTheDocument();
  });
  
  it('should show error when submitting empty form', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);
    
    fireEvent.click(screen.getByRole('button', { name: '登录' }));
    
    expect(screen.getByText('请输入邮箱和密码')).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });
  
  it('should show error when submitting invalid email', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);
    
    fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'invalid-email' } });
    fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password123' } });
    fireEvent.click(screen.getByRole('button', { name: '登录' }));
    
    expect(screen.getByText('请输入有效的邮箱地址')).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });
  
  it('should call onSubmit with correct data when form is valid', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);
    
    fireEvent.change(screen.getByLabelText('邮箱'), { target: { value: 'test@example.com' } });
    fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'password123' } });
    fireEvent.click(screen.getByRole('button', { name: '登录' }));
    
    expect(mockOnSubmit).toHaveBeenCalledTimes(1);
    expect(mockOnSubmit).toHaveBeenCalledWith({ 
      email: 'test@example.com', 
      password: 'password123' 
    });
    expect(screen.queryByText('请输入邮箱和密码')).not.toBeInTheDocument();
  });
});

案例3:API 集成测试与 Mock ​

javascript
// 源文件:src/api/user.js
import axios from 'axios';

export const fetchUser = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

export const updateUser = async (userId, data) => {
  const response = await axios.put(`/api/users/${userId}`, data);
  return response.data;
};

// 测试文件:src/api/__tests__/user.test.js
import axios from 'axios';
import { fetchUser, updateUser } from '../user';

// Mock axios
jest.mock('axios');
const mockedAxios = axios;

describe('user API', () => {
  describe('fetchUser', () => {
    it('should fetch user data successfully', async () => {
      // 模拟成功响应
      const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' };
      mockedAxios.get.mockResolvedValueOnce({ data: mockUser });
      
      const result = await fetchUser(1);
      
      expect(mockedAxios.get).toHaveBeenCalledTimes(1);
      expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/1');
      expect(result).toEqual(mockUser);
    });
    
    it('should handle error when fetching user fails', async () => {
      // 模拟失败响应
      mockedAxios.get.mockRejectedValueOnce(new Error('Network Error'));
      
      await expect(fetchUser(1)).rejects.toThrow('Network Error');
      expect(mockedAxios.get).toHaveBeenCalledTimes(1);
    });
  });
  
  describe('updateUser', () => {
    it('should update user data successfully', async () => {
      const mockUser = { id: 1, name: 'Updated Name', email: 'updated@example.com' };
      const updateData = { name: 'Updated Name' };
      
      mockedAxios.put.mockResolvedValueOnce({ data: mockUser });
      
      const result = await updateUser(1, updateData);
      
      expect(mockedAxios.put).toHaveBeenCalledTimes(1);
      expect(mockedAxios.put).toHaveBeenCalledWith('/api/users/1', updateData);
      expect(result).toEqual(mockUser);
    });
  });
});

4. Jest 最佳实践 ​

  1. 测试命名规范:使用 describe 和 it 组织测试,描述清晰的测试场景
  2. 测试隔离:每个测试用例应该独立运行,避免依赖关系
  3. Mock 策略:只 Mock 外部依赖(API、第三方库),不 Mock 内部逻辑
  4. 快照测试:用于UI组件的结构测试,定期更新快照
  5. 测试覆盖率:关注核心业务逻辑的覆盖率,不盲目追求100%
  6. 异步测试:优先使用 async/await,避免回调地狱
  7. 测试环境:根据测试对象选择合适的环境(jsdom/node)

三、Playwright 深度详解 ​

1. 核心概念与优势 ​

Playwright 是 Microsoft 开源的现代化 E2E(端到端)测试框架,以其跨浏览器支持、自动等待、强大的选择器等特性成为E2E测试的新宠。

核心优势:

  • 跨浏览器支持:Chrome、Firefox、Safari、Edge
  • 自动等待:无需手动添加等待时间
  • 强大的选择器:支持CSS、XPath、文本、属性等
  • 网络请求拦截与Mock
  • 移动设备模拟
  • 并行测试支持
  • 可视化测试与截图对比

2. 企业级配置 ​

javascript
// playwright.config.js
const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
  // 测试目录
  testDir: './tests',
  
  // 测试文件匹配规则
  testMatch: /.*\.spec\.js/,
  
  // 全局超时
  timeout: 30 * 1000,
  
  // 单个测试用例超时
  expect: {
    timeout: 5000
  },
  
  // 并行测试配置
  fullyParallel: true,
  workers: process.env.CI ? 2 : undefined,
  
  // 失败重试
  retries: process.env.CI ? 2 : 0,
  
  // 测试报告
  reporter: [
    ['html'], // 生成HTML报告
    ['json', { outputFile: 'test-results.json' }] // 生成JSON报告
  ],
  
  // 测试环境
  use: {
    // 基础URL
    baseURL: 'http://localhost:3000',
    
    // 浏览器配置
    browserName: 'chromium',
    headless: process.env.CI ? true : false,
    
    // 视口大小
    viewport: { width: 1280, height: 720 },
    
    // 截图配置
    screenshot: 'only-on-failure',
    
    // 视频配置
    video: 'retain-on-failure',
    
    // 跟踪配置(用于调试)
    trace: 'retain-on-failure',
    
    // 存储身份验证状态
    storageState: 'storageState.json'
  },
  
  // 项目配置(多浏览器测试)
  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'] },
    },
  ],
  
  // 开发服务器配置(自动启动)
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    timeout: 120 * 1000,
    reuseExistingServer: !process.env.CI,
  },
});

3. 经典案例:企业级E2E测试 ​

案例1:用户登录与仪表盘访问 ​

javascript
// tests/auth.spec.js
const { test, expect } = require('@playwright/test');

test.describe('用户认证流程', () => {
  test('用户应该能够成功登录并访问仪表盘', async ({ page }) => {
    // 访问登录页
    await page.goto('/login');
    
    // 验证登录页加载成功
    await expect(page).toHaveTitle(/登录/);
    await expect(page.locator('h2')).toContainText('登录');
    
    // 输入无效凭据
    await page.fill('#email', 'invalid@example.com');
    await page.fill('#password', 'wrongpassword');
    await page.click('button[type="submit"]');
    
    // 验证错误提示
    await expect(page.locator('.error')).toContainText('邮箱或密码错误');
    
    // 输入有效凭据
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    
    // 验证登录成功并跳转到仪表盘
    await expect(page).toHaveURL('/dashboard');
    await expect(page).toHaveTitle(/仪表盘/);
    await expect(page.locator('.welcome-message')).toContainText('欢迎回来');
  });
  
  test('未登录用户应该被重定向到登录页', async ({ page }) => {
    // 直接访问受保护页面
    await page.goto('/dashboard');
    
    // 验证被重定向到登录页
    await expect(page).toHaveURL(/login/);
    await expect(page.locator('h2')).toContainText('登录');
  });
});

案例2:购物车功能测试 ​

javascript
// tests/shopping-cart.spec.js
const { test, expect } = require('@playwright/test');

test.describe('购物车功能', () => {
  test('用户应该能够添加商品到购物车', async ({ page }) => {
    // 登录
    await page.goto('/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    
    // 访问商品列表
    await page.goto('/products');
    
    // 添加第一个商品到购物车
    const firstProductAddButton = page.locator('.product-card').first().locator('button', { hasText: '添加到购物车' });
    await firstProductAddButton.click();
    
    // 验证购物车计数更新
    await expect(page.locator('.cart-count')).toContainText('1');
    
    // 添加第二个商品到购物车
    const secondProductAddButton = page.locator('.product-card').nth(1).locator('button', { hasText: '添加到购物车' });
    await secondProductAddButton.click();
    
    // 验证购物车计数更新
    await expect(page.locator('.cart-count')).toContainText('2');
    
    // 查看购物车
    await page.click('.cart-icon');
    
    // 验证购物车页面显示正确
    await expect(page.locator('.cart-item')).toHaveCount(2);
    
    // 验证购物车总价计算正确
    const prices = await page.locator('.cart-item-price').allTextContents();
    const totalPrice = prices.reduce((sum, price) => {
      return sum + parseFloat(price.replace('¥', ''));
    }, 0);
    
    const displayedTotal = await page.locator('.cart-total').textContent();
    expect(parseFloat(displayedTotal.replace('总价:¥', ''))).toBe(totalPrice);
  });
  
  test('用户应该能够从购物车中移除商品', async ({ page }) => {
    // 登录并添加商品到购物车(复用上面的登录逻辑)
    // ...
    
    // 访问购物车
    await page.goto('/cart');
    
    // 验证购物车中有商品
    await expect(page.locator('.cart-item')).toHaveCount(2);
    
    // 移除第一个商品
    await page.locator('.cart-item').first().locator('.remove-button').click();
    
    // 验证商品被移除
    await expect(page.locator('.cart-item')).toHaveCount(1);
    await expect(page.locator('.cart-count')).toContainText('1');
  });
});

案例3:视觉回归测试 ​

javascript
// tests/visual-regression.spec.js
const { test, expect } = require('@playwright/test');

test.describe('视觉回归测试', () => {
  test('登录页面应该保持一致的视觉效果', async ({ page }) => {
    await page.goto('/login');
    
    // 截取登录页面的截图并与基准图比较
    await expect(page).toHaveScreenshot('login-page.png', {
      fullPage: true,
      maxDiffPixels: 10, // 允许10个像素的差异
      maxDiffPixelRatio: 0.01 // 允许1%的像素差异
    });
  });
  
  test('产品卡片应该保持一致的视觉效果', async ({ page }) => {
    await page.goto('/products');
    
    // 截取第一个产品卡片的截图
    const firstProductCard = page.locator('.product-card').first();
    await expect(firstProductCard).toHaveScreenshot('product-card.png');
  });
});

案例4:网络请求拦截与Mock ​

javascript
// tests/mock-network.spec.js
const { test, expect } = require('@playwright/test');

test.describe('网络请求Mock', () => {
  test('应该能够Mock API请求并显示模拟数据', async ({ page }) => {
    // 拦截并Mock产品列表API
    await page.route('**/api/products', async (route) => {
      // 返回模拟数据
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Mock Product 1', price: 100, category: 'electronics' },
          { id: 2, name: 'Mock Product 2', price: 200, category: 'clothing' }
        ])
      });
    });
    
    // 访问产品列表页
    await page.goto('/products');
    
    // 验证显示的是模拟数据
    await expect(page.locator('.product-card')).toHaveCount(2);
    await expect(page.locator('.product-card').first()).toContainText('Mock Product 1');
    await expect(page.locator('.product-card').nth(1)).toContainText('Mock Product 2');
  });
  
  test('应该能够处理API错误场景', async ({ page }) => {
    // 拦截并模拟API错误
    await page.route('**/api/products', async (route) => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: '服务器错误' })
      });
    });
    
    // 访问产品列表页
    await page.goto('/products');
    
    // 验证错误提示
    await expect(page.locator('.error-message')).toContainText('服务器错误');
    await expect(page.locator('.product-card')).toHaveCount(0);
  });
});

3. Playwright 最佳实践 ​

  1. 选择稳定的选择器:优先使用 role、data-testid 等稳定的选择器,避免依赖CSS类名
  2. 自动等待:充分利用Playwright的自动等待功能,避免手动添加 waitForTimeout
  3. 测试隔离:每个测试用例应该独立运行,避免依赖其他测试的状态
  4. 数据清理:测试后清理创建的数据,保持测试环境的清洁
  5. 并行测试:启用并行测试提高执行效率
  6. 失败重试:在CI环境中配置适当的重试机制
  7. 视觉回归测试:结合截图对比检测UI变化
  8. 网络请求管理:合理使用网络请求拦截和Mock

四、企业级测试策略 ​

1. 测试金字塔的实际应用 ​

在企业级项目中,遵循测试金字塔原则可以达到测试效率和质量的最佳平衡:

测试类型占比框架特点
单元测试70%Jest测试单个函数/组件,速度快,成本低
集成测试20%Jest + 工具库测试组件间/模块间交互,验证数据流
E2E测试10%Playwright模拟真实用户行为,验证完整业务流程

2. CI/CD 集成 ​

将测试集成到CI/CD流程中,实现自动化测试,确保每次代码变更都经过验证:

yaml
# .github/workflows/test.yml
name: 自动化测试

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: 设置Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: 安装依赖
        run: npm install
      
      - name: 运行单元测试
        run: npm run test:unit
        env:
          CI: true
      
      - name: 构建应用
        run: npm run build
      
      - name: 运行E2E测试
        run: npm run test:e2e
        env:
          CI: true
      
      - name: 上传测试报告
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: test-reports
          path: |
            coverage/
            playwright-report/
            test-results.json
      
      - name: 上传覆盖率报告到Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

3. 测试覆盖率管理 ​

  • 设置合理的覆盖率目标:通常80%左右是比较合理的目标
  • 关注核心业务逻辑:优先保证核心功能的覆盖率
  • 避免盲目追求100%:测试覆盖率不是唯一指标,更重要的是测试质量
  • 定期审查覆盖率报告:识别测试盲区,持续优化测试用例

五、常见问题与解决方案 ​

1. Jest 常见问题 ​

问题解决方案
异步测试失败使用 async/await,确保测试等待异步操作完成
Mock 函数不生效确保在测试前Mock,使用 jest.mock() 或 jest.spyOn()
样式文件导入错误在 moduleNameMapper 中配置样式文件的Mock
测试环境不匹配根据测试对象选择正确的测试环境(jsdom/node)
测试速度慢启用并行测试,减少不必要的渲染,清理测试环境

2. Playwright 常见问题 ​

问题解决方案
测试不稳定使用稳定的选择器,利用自动等待,避免硬编码等待时间
跨浏览器兼容性问题在多个浏览器中运行测试,使用Playwright的跨浏览器支持
测试数据管理使用测试前准备数据,测试后清理数据,或使用Mock数据
大型测试套件执行时间长启用并行测试,优化测试顺序,拆分测试套件
视觉回归测试误报调整截图比较的阈值,确保测试环境一致

六、面试题解析 ​

1. Jest 相关面试题 ​

Q1:Jest 中 toBe 和 toEqual 的区别是什么?

A:

  • toBe 使用 === 进行严格相等比较,适合基本类型和引用比较
  • toEqual 进行深度相等比较,适合对象和数组等复杂类型

Q2:如何在 Jest 中 Mock 一个模块?

A:

  • 使用 jest.mock('module-name') 自动Mock整个模块
  • 使用 jest.spyOn(object, 'method') Mock对象的特定方法
  • 自定义Mock实现:jest.mock('module-name', () => ({ /* 自定义实现 */ }))

Q3:什么是 Jest 的快照测试?

A: 快照测试是一种验证UI组件渲染结果的方法,Jest会将组件渲染结果保存为快照文件,每次测试时比较当前渲染结果与快照是否一致。当UI发生预期变化时,需要使用 jest --updateSnapshot 更新快照。

Q4:如何测试异步代码?

A:

  • 使用 async/await:test('async test', async () => { const result = await fetchData(); expect(result).toBe('data'); });
  • 使用 resolves/rejects:test('async test', () => expect(fetchData()).resolves.toBe('data'));
  • 使用回调:test('async test', (done) => { fetchData().then(result => { expect(result).toBe('data'); done(); }); });

2. Playwright 相关面试题 ​

Q1:Playwright 相比 Selenium 有什么优势?

A:

  • 自动等待功能,无需手动添加等待时间
  • 跨浏览器支持更好,包括Chrome、Firefox、Safari、Edge
  • 更现代的API设计,支持异步/await
  • 强大的选择器系统
  • 网络请求拦截和Mock功能更完善
  • 并行测试支持更好

Q2:Playwright 中的 page 和 browser 是什么关系?

A:

  • browser 是浏览器实例,代表一个浏览器进程
  • context 是浏览器上下文,可以理解为一个隐身窗口
  • page 是浏览器页面,代表一个标签页
  • 关系:browser → context → page

Q3:如何处理Playwright测试中的认证?

A:

  • 使用 storageState 保存和恢复认证状态
  • 编写认证辅助函数,在测试前登录
  • 使用 page.context().storageState({ path: 'auth.json' }) 保存认证状态
  • 在测试配置中使用 storageState: 'auth.json' 恢复认证状态

Q4:什么是Playwright的视觉回归测试?

A: 视觉回归测试是通过比较页面或组件的截图,检测UI是否发生变化的测试方法。Playwright提供了 toHaveScreenshot() 方法,可以轻松实现视觉回归测试。

七、总结与快速上手指南 ​

1. 快速选择测试框架 ​

测试类型推荐框架特点
单元测试Jest配置简单、功能全面、生态丰富
组件测试Jest + React Testing Library/@vue/test-utils贴近用户行为,易于维护
E2E测试Playwright现代化、跨浏览器、自动等待

2. 快速上手步骤 ​

Jest 快速上手:

bash
# 1. 安装
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

# 2. 配置 babel.config.js
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

# 3. 编写测试(sum.test.js)
function sum(a, b) {
  return a + b;
}

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

# 4. 运行测试
npx jest

Playwright 快速上手:

bash
# 1. 安装
npm install --save-dev playwright @playwright/test

# 2. 初始化配置
npx playwright init

# 3. 编写测试(example.spec.js)
const { test, expect } = require('@playwright/test');

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

# 4. 运行测试
npx playwright test

3. 企业级最佳实践 ​

  • 测试分层:遵循金字塔模型,合理分配测试资源
  • 测试命名:清晰描述测试场景,便于维护
  • 测试隔离:每个测试用例独立运行,避免依赖
  • 模拟外部依赖:使用Mock隔离外部系统,提高测试稳定性
  • 定期运行:在CI/CD中自动运行,确保代码质量
  • 覆盖率监控:设置合理的覆盖率目标,持续优化
  • 测试评审:与代码评审一起进行,提高测试质量
  • 可视化报告:生成易于理解的测试报告,便于团队协作

八、结语 ​

前端测试是保障Web应用质量的关键环节,Jest 和 Playwright 作为当前最流行的测试框架,各有所长,相互补充。通过掌握这两个工具,你可以构建完整的测试体系,从单元测试到E2E测试,全面保障应用质量。

在企业级开发中,测试不仅仅是编写测试用例,更是一种工程化思维和质量文化。希望本文能够帮助你快速掌握前端测试的核心技能,在面试和工作中脱颖而出。

Updated at:

Released under the MIT License.