React 测试入门:Jest + Testing Library 完整指南

引言

在 React 开发中,测试是保证代码质量的重要环节。良好的测试体系能够帮助我们:

  • 快速发现回归问题
  • 安全重构代码
  • 文档化组件行为
  • 提升开发信心

本文将介绍 React 测试的核心工具 Jest 和 Testing Library,并通过实战示例带你入门。

测试工具简介

Jest

Jest 是 Facebook 出品的 JavaScript 测试框架,具有以下特点:

  • 零配置:开箱即用
  • 快照测试:自动检测 UI 变化
  • Mock 功能:轻松模拟依赖
  • 并行 执行:提升测试速度

Testing Library

Testing Library 是一套测试工具集合,核心理念是:

测试应该像用户一样使用你的应用

它鼓励我们测试组件的行为 而非实现细节,主要包含:

  • @testing-library/react:React 组件测试
  • @testing-library/user-event:模拟用户交互
  • @testing-library/jest-dom:DOM 匹配器

环境搭建

首先安装必要的依赖:

dart 复制代码
npm install -D jest @testing-library/react @testing-library/jest-dom @testing-library/user-event

创建 jest.setup.js 配置文件:

kotlin 复制代码
import '@testing-library/jest-dom';

// 全局匹配器
expect.extend({
  // 自定义匹配器
});

在 package.json 中添加测试脚本:

json 复制代码
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

基础测试示例

测试简单组件

假设我们有一个简单的 Button 组件:

javascript 复制代码
// Button.jsx
export default function Button({ children, onClick, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
}

编写测试用例:

ini 复制代码
// Button.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';

describe('Button 组件', () => {
  test('渲染按钮文本', () => {
    render(<Button>点击我</Button>);
    expect(screen.getByText('点击我')).toBeInTheDocument();
  });

  test('禁用状态下不可点击', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
    
    render(<Button disabled onClick={handleClick}>禁用按钮</Button>);
    
    const button = screen.getByText('禁用按钮');
    expect(button).toBeDisabled();
    
    await user.click(button);
    expect(handleClick).not.toHaveBeenCalled();
  });

  test('点击时触发回调', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
    
    render(<Button onClick={handleClick}>点击</Button>);
    
    await user.click(screen.getByText('点击'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

测试异步操作

实际项目中,组件经常涉及异步操作。Testing Library 提供了 findBy 和 waitFor 来处理异步场景。

示例:数据获取组件

javascript 复制代码
// UserProfile.jsx
import { useState, useEffect } from 'react';

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>加载中...</div>;
  if (!user) return <div>未找到用户</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

异步测试写法

javascript 复制代码
// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';

// Mock fetch
global.fetch = jest.fn();

describe('UserProfile 组件', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('显示加载状态', () => {
    fetch.mockResolvedValueOnce({
      json: async () => ({ name: '张三', email: 'zhang@example.com' })
    });

    render(<UserProfile userId="123" />);
    
    expect(screen.getByText('加载中...')).toBeInTheDocument();
  });

  test('成功加载后显示用户信息', async () => {
    fetch.mockResolvedValueOnce({
      json: async () => ({ name: '张三', email: 'zhang@example.com' })
    });

    render(<UserProfile userId="123" />);
    
    // 使用 findBy 等待元素出现
    const nameElement = await screen.findByText('张三');
    expect(nameElement).toBeInTheDocument();
    expect(screen.getByText('zhang@example.com')).toBeInTheDocument();
  });

  test('加载失败显示错误信息', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'));

    render(<UserProfile userId="123" />);
    
    await waitFor(() => {
      expect(screen.queryByText('加载中...')).not.toBeInTheDocument();
    });
  });
});

测试自定义 Hooks

自定义 Hooks 的测试需要特殊处理。我们可以创建一个测试组件来包裹 Hook。

ini 复制代码
// useCounter.js
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return { count, increment, decrement, reset };
}
erlang 复制代码
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  test('初始值为 0', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  test('使用自定义初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  test('increment 增加计数', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  test('decrement 减少计数', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });

  test('reset 重置为初始值', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });
});

常用匹配器

Testing Library 配合 jest-dom 提供丰富的匹配器:

scss 复制代码
// 存在性检查
toBeInTheDocument();
not.toBeInTheDocument();

// 可见性检查
toBeVisible();
not.toBeVisible();

// 状态检查
toBeDisabled();
toBeEnabled();
toBeChecked();
toHaveFocus();

// 内容检查
toHaveTextContent('文本');
toHaveAttribute('href', '/link');
toHaveClass('active');
toHaveStyle({ color: 'red' });

// 数量检查
toHaveLength(3);

最佳实践

1. 测试行为而非实现

scss 复制代码
// ❌ 不推荐:测试实现细节
expect(component.props.className).toBe('btn-primary');

// ✅ 推荐:测试用户可见的行为
expect(screen.getByRole('button')).toHaveClass('btn-primary');

2. 使用语义化查询

scss 复制代码
// 优先级顺序
getByRole()        // 按 ARIA 角色
getByLabelText()   // 按标签文本
getByPlaceholderText() // 按占位符
getByText()        // 按文本内容
getByTestId()      // 最后选择(添加 data-testid)

3. 保持测试独立

scss 复制代码
// 每个测试应该独立运行
beforeEach(() => {
  jest.clearAllMocks();
  cleanup();
});

4. 测试边界情况

  • 空状态
  • 加载状态
  • 错误状态
  • 极端输入值

总结

React 测试是保证应用质量的关键环节。掌握 Jest 和 Testing Library 的核心用法,遵循"测试行为而非实现"的原则,能够帮助我们构建更可靠的应用。

核心要点回顾:

  1. 使用 Testing Library 模拟用户行为
  2. 优先使用语义化查询(getByRole 等)
  3. 异步操作使用 findBy 或 waitFor
  4. 测试 Hooks 使用 renderHook
  5. 保持测试独立、可维护

开始为你的 React 项目添加测试吧,这将是值得的投资!

相关推荐
卷无止境1 小时前
ECharts:把数据变成故事的可视化利器
前端
Eric_见嘉2 小时前
在职前端 Skill 和 MCP 分享
前端·后端·agent
liangshanbo12152 小时前
面试题:如何优化 Webpack 的打包速度?
前端·webpack·node.js
wengad2 小时前
从 wangEditor 迁到 wangEditor-next
前端·javascript·vue.js
行者-全栈开发3 小时前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
计算机魔术师4 小时前
AMD砸82亿买下李飞飞,这次不卖芯片卖什么?
前端
hzxpaipai5 小时前
杭州企业官网怎么建设?从策划到上线的5步建站方法
运维·服务器·前端·网络
怕浪猫5 小时前
2026年金九银十,我面了22个前端
前端·javascript·面试
IT_陈寒5 小时前
Vue 这个响应式陷阱,我的头发都掉没了
前端·人工智能·后端
前端的日常5 小时前
一个人+AI做情侣食谱小程序,30天纯赚
前端·javascript·后端