React 18.x 学习计划 - 第八天:React测试

学习目标

  • 掌握Jest测试框架
  • 学会使用React Testing Library
  • 理解单元测试和集成测试
  • 掌握Mock和测试工具
  • 构建完整的测试套件

学习时间安排

总时长:8-9小时

  • 测试环境搭建:1小时
  • Jest基础:2小时
  • React Testing Library:2.5小时
  • 高级测试技巧:1.5小时
  • 实践项目:2-3小时

第一部分:测试环境搭建 (1小时)

1.1 安装测试依赖

安装测试工具
bash 复制代码
# 安装Jest和React Testing Library
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

# 安装Jest相关依赖
npm install --save-dev jest jest-environment-jsdom @types/jest

# 安装测试覆盖率工具
npm install --save-dev @jest/coverage

# 如果使用TypeScript
npm install --save-dev @types/testing-library__react @types/testing-library__jest-dom

1.2 Jest配置

Jest配置文件
javascript 复制代码
// jest.config.js
module.exports = {
  // 测试环境
  testEnvironment: 'jsdom',
  
  // 设置文件,在每个测试文件运行前执行
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  
  // 模块文件扩展名
  moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],
  
  // 模块名称映射
  moduleNameMapper: {
    // 处理CSS模块
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    // 处理静态资源
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js'
  },
  
  // 转换配置
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
  },
  
  // 测试文件匹配模式
  testMatch: [
    '**/__tests__/**/*.(js|jsx|ts|tsx)',
    '**/*.(test|spec).(js|jsx|ts|tsx)'
  ],
  
  // 收集覆盖率的文件
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/reportWebVitals.js'
  ],
  
  // 覆盖率阈值
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70
    }
  },
  
  // 覆盖率报告格式
  coverageReporters: ['text', 'lcov', 'html'],
  
  // 清除模拟
  clearMocks: true,
  
  // 恢复模拟
  restoreMocks: true
};
测试设置文件
javascript 复制代码
// src/setupTests.js
// 导入Jest DOM匹配器
// 这些匹配器扩展了Jest的断言功能,专门用于DOM测试
import '@testing-library/jest-dom';

// 配置测试环境
// 设置全局测试配置

// Mock window.matchMedia
// 某些组件可能使用matchMedia API,需要在测试环境中模拟
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

// Mock IntersectionObserver
// 某些组件可能使用IntersectionObserver API,需要在测试环境中模拟
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  takeRecords() {
    return [];
  }
  unobserve() {}
};

// Mock ResizeObserver
// 某些组件可能使用ResizeObserver API,需要在测试环境中模拟
global.ResizeObserver = class ResizeObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  unobserve() {}
};

// 配置测试超时时间
jest.setTimeout(10000);

// 全局测试工具函数
global.testUtils = {
  // 等待异步操作完成
  waitFor: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
  
  // 创建模拟用户
  createMockUser: (overrides = {}) => ({
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  }),
  
  // 创建模拟数据
  createMockData: (count = 10) => {
    return Array.from({ length: count }, (_, i) => ({
      id: i + 1,
      name: `Item ${i + 1}`,
      value: Math.random() * 100
    }));
  }
};

第二部分:Jest基础 (2小时)

2.1 Jest基础语法

基础测试示例
javascript 复制代码
// src/utils/__tests__/math.test.js
// 导入要测试的函数
import { add, subtract, multiply, divide } from '../math';

// describe函数用于组织相关测试
// 第一个参数是测试套件的描述
// 第二个参数是包含测试用例的函数
describe('Math utilities', () => {
  // test或it函数用于定义单个测试用例
  // 第一个参数是测试用例的描述
  // 第二个参数是测试函数
  test('add function should add two numbers correctly', () => {
    // expect函数用于创建断言
    // toBe是匹配器,用于检查值是否严格相等
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });

  test('subtract function should subtract two numbers correctly', () => {
    expect(subtract(5, 3)).toBe(2);
    expect(subtract(0, 5)).toBe(-5);
    expect(subtract(10, 10)).toBe(0);
  });

  test('multiply function should multiply two numbers correctly', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-2, 3)).toBe(-6);
    expect(multiply(0, 5)).toBe(0);
  });

  test('divide function should divide two numbers correctly', () => {
    expect(divide(6, 3)).toBe(2);
    expect(divide(10, 2)).toBe(5);
    expect(divide(-6, 3)).toBe(-2);
  });

  test('divide function should throw error when dividing by zero', () => {
    // toThrow匹配器用于检查函数是否抛出错误
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});
常用匹配器
javascript 复制代码
// src/utils/__tests__/matchers.test.js
describe('Jest Matchers', () => {
  test('equality matchers', () => {
    // toBe用于严格相等比较(使用Object.is)
    expect(2 + 2).toBe(4);
    
    // toEqual用于深度相等比较(递归比较对象属性)
    expect({ name: 'John', age: 30 }).toEqual({ name: 'John', age: 30 });
    
    // not用于取反
    expect(2 + 2).not.toBe(5);
  });

  test('truthiness matchers', () => {
    // toBeTruthy检查值是否为真值
    expect(true).toBeTruthy();
    expect(1).toBeTruthy();
    expect('hello').toBeTruthy();
    
    // toBeFalsy检查值是否为假值
    expect(false).toBeFalsy();
    expect(0).toBeFalsy();
    expect('').toBeFalsy();
    expect(null).toBeFalsy();
    expect(undefined).toBeFalsy();
  });

  test('number matchers', () => {
    const value = 2 + 2;
    
    // toBeGreaterThan检查值是否大于
    expect(value).toBeGreaterThan(3);
    
    // toBeGreaterThanOrEqual检查值是否大于等于
    expect(value).toBeGreaterThanOrEqual(4);
    
    // toBeLessThan检查值是否小于
    expect(value).toBeLessThan(5);
    
    // toBeLessThanOrEqual检查值是否小于等于
    expect(value).toBeLessThanOrEqual(4);
    
    // toBeCloseTo用于浮点数比较(避免精度问题)
    expect(0.1 + 0.2).toBeCloseTo(0.3);
  });

  test('string matchers', () => {
    const str = 'Hello World';
    
    // toMatch用于正则表达式匹配
    expect(str).toMatch(/Hello/);
    expect(str).toMatch('World');
    
    // toContain检查字符串是否包含子字符串
    expect(str).toContain('Hello');
  });

  test('array matchers', () => {
    const arr = [1, 2, 3, 4, 5];
    
    // toContain检查数组是否包含元素
    expect(arr).toContain(3);
    expect(arr).not.toContain(6);
    
    // toHaveLength检查数组长度
    expect(arr).toHaveLength(5);
  });

  test('object matchers', () => {
    const obj = {
      name: 'John',
      age: 30,
      email: 'john@example.com'
    };
    
    // toHaveProperty检查对象是否具有属性
    expect(obj).toHaveProperty('name');
    expect(obj).toHaveProperty('age', 30);
    
    // toMatchObject检查对象是否匹配部分属性
    expect(obj).toMatchObject({ name: 'John', age: 30 });
  });

  test('exception matchers', () => {
    // toThrow检查函数是否抛出错误
    expect(() => {
      throw new Error('Something went wrong');
    }).toThrow();
    
    expect(() => {
      throw new Error('Something went wrong');
    }).toThrow('Something went wrong');
    
    expect(() => {
      throw new Error('Something went wrong');
    }).toThrow(Error);
  });
});

2.2 异步测试

异步测试示例
javascript 复制代码
// src/utils/__tests__/async.test.js
// 导入要测试的异步函数
import { fetchData, fetchUser, processData } from '../async';

describe('Async functions', () => {
  // 测试Promise
  test('fetchData should return data', () => {
    // 返回Promise,Jest会等待Promise解决
    return fetchData().then(data => {
      expect(data).toBeDefined();
      expect(data).toHaveProperty('id');
      expect(data).toHaveProperty('name');
    });
  });

  // 使用async/await测试异步函数
  test('fetchUser should return user data', async () => {
    const user = await fetchUser(1);
    
    expect(user).toBeDefined();
    expect(user.id).toBe(1);
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('email');
  });

  // 测试异步错误
  test('fetchUser should throw error for invalid id', async () => {
    // 使用rejects匹配器测试Promise拒绝
    await expect(fetchUser(999)).rejects.toThrow('User not found');
  });

  // 测试多个异步操作
  test('processData should process multiple items', async () => {
    const items = [1, 2, 3];
    const results = await Promise.all(
      items.map(item => processData(item))
    );
    
    expect(results).toHaveLength(3);
    results.forEach(result => {
      expect(result).toBeDefined();
      expect(result).toHaveProperty('processed');
    });
  });

  // 使用waitFor测试异步更新
  test('should update state asynchronously', async () => {
    const { waitFor } = require('@testing-library/react');
    
    let state = { value: 0 };
    
    // 模拟异步更新
    setTimeout(() => {
      state.value = 100;
    }, 100);
    
    // 等待状态更新
    await waitFor(() => {
      expect(state.value).toBe(100);
    });
  });
});

第三部分:React Testing Library (2.5小时)

3.1 组件渲染测试

基础组件测试
javascript 复制代码
// src/components/__tests__/Button.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent } from '@testing-library/react';
// 导入要测试的组件
import Button from '../Button';

// describe函数组织测试套件
describe('Button Component', () => {
  // 测试基本渲染
  test('should render button with text', () => {
    // render函数渲染组件到DOM
    render(<Button>Click me</Button>);
    
    // screen对象提供查询DOM的方法
    // getByText查找包含指定文本的元素
    const button = screen.getByText('Click me');
    
    // 断言按钮存在
    expect(button).toBeInTheDocument();
  });

  // 测试按钮点击事件
  test('should call onClick when clicked', () => {
    // 创建模拟函数
    const handleClick = jest.fn();
    
    // 渲染组件并传递onClick处理函数
    render(<Button onClick={handleClick}>Click me</Button>);
    
    // 查找按钮元素
    const button = screen.getByText('Click me');
    
    // fireEvent模拟用户事件
    fireEvent.click(button);
    
    // 断言函数被调用
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  // 测试按钮禁用状态
  test('should be disabled when disabled prop is true', () => {
    render(<Button disabled>Disabled Button</Button>);
    
    const button = screen.getByText('Disabled Button');
    
    // 断言按钮被禁用
    expect(button).toBeDisabled();
  });

  // 测试按钮样式类
  test('should apply custom className', () => {
    render(<Button className="custom-class">Button</Button>);
    
    const button = screen.getByText('Button');
    
    // 断言按钮具有自定义类名
    expect(button).toHaveClass('custom-class');
  });

  // 测试按钮类型
  test('should render with correct type', () => {
    render(<Button type="submit">Submit</Button>);
    
    const button = screen.getByText('Submit');
    
    // 断言按钮类型正确
    expect(button).toHaveAttribute('type', 'submit');
  });
});

3.2 表单测试

表单组件测试
javascript 复制代码
// src/components/__tests__/Form.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
// 导入用户事件库
import userEvent from '@testing-library/user-event';
// 导入要测试的组件
import Form from '../Form';

describe('Form Component', () => {
  // 测试表单渲染
  test('should render form with all fields', () => {
    render(<Form />);
    
    // 使用getByLabelText查找标签关联的输入框
    expect(screen.getByLabelText('Name:')).toBeInTheDocument();
    expect(screen.getByLabelText('Email:')).toBeInTheDocument();
    expect(screen.getByLabelText('Message:')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
  });

  // 测试表单输入
  test('should update input values when user types', async () => {
    // 创建用户事件实例
    const user = userEvent.setup();
    
    render(<Form />);
    
    // 获取输入框
    const nameInput = screen.getByLabelText('Name:');
    const emailInput = screen.getByLabelText('Email:');
    const messageInput = screen.getByLabelText('Message:');
    
    // 使用userEvent.type模拟用户输入
    await user.type(nameInput, 'John Doe');
    await user.type(emailInput, 'john@example.com');
    await user.type(messageInput, 'Hello World');
    
    // 断言输入值已更新
    expect(nameInput).toHaveValue('John Doe');
    expect(emailInput).toHaveValue('john@example.com');
    expect(messageInput).toHaveValue('Hello World');
  });

  // 测试表单提交
  test('should submit form with correct data', async () => {
    const handleSubmit = jest.fn();
    const user = userEvent.setup();
    
    render(<Form onSubmit={handleSubmit} />);
    
    // 填写表单
    await user.type(screen.getByLabelText('Name:'), 'John Doe');
    await user.type(screen.getByLabelText('Email:'), 'john@example.com');
    await user.type(screen.getByLabelText('Message:'), 'Hello World');
    
    // 提交表单
    await user.click(screen.getByRole('button', { name: 'Submit' }));
    
    // 等待异步操作完成
    await waitFor(() => {
      expect(handleSubmit).toHaveBeenCalledWith({
        name: 'John Doe',
        email: 'john@example.com',
        message: 'Hello World'
      });
    });
  });

  // 测试表单验证
  test('should show validation errors for empty fields', async () => {
    const user = userEvent.setup();
    
    render(<Form />);
    
    // 直接提交空表单
    await user.click(screen.getByRole('button', { name: 'Submit' }));
    
    // 等待验证错误显示
    await waitFor(() => {
      expect(screen.getByText('Name is required')).toBeInTheDocument();
      expect(screen.getByText('Email is required')).toBeInTheDocument();
    });
  });

  // 测试表单重置
  test('should reset form after successful submission', async () => {
    const handleSubmit = jest.fn().mockResolvedValue({ success: true });
    const user = userEvent.setup();
    
    render(<Form onSubmit={handleSubmit} />);
    
    // 填写并提交表单
    await user.type(screen.getByLabelText('Name:'), 'John Doe');
    await user.type(screen.getByLabelText('Email:'), 'john@example.com');
    await user.click(screen.getByRole('button', { name: 'Submit' }));
    
    // 等待表单重置
    await waitFor(() => {
      expect(screen.getByLabelText('Name:')).toHaveValue('');
      expect(screen.getByLabelText('Email:')).toHaveValue('');
    });
  });
});

3.3 Hooks测试

Hooks测试示例
javascript 复制代码
// src/hooks/__tests__/useCounter.test.js
// 导入React和测试工具
import { renderHook, act } from '@testing-library/react';
// 导入要测试的Hook
import { useCounter } from '../useCounter';

describe('useCounter Hook', () => {
  // 测试初始值
  test('should initialize with default value', () => {
    // renderHook用于测试自定义Hook
    const { result } = renderHook(() => useCounter());
    
    // result.current包含Hook的返回值
    expect(result.current.count).toBe(0);
  });

  // 测试初始值设置
  test('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    
    expect(result.current.count).toBe(10);
  });

  // 测试增加计数
  test('should increment count', () => {
    const { result } = renderHook(() => useCounter(0));
    
    // act函数包装状态更新,确保更新完成后再断言
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(2);
  });

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

  // 测试重置计数
  test('should reset count to initial value', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.increment();
    });
    
    expect(result.current.count).toBe(12);
    
    act(() => {
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });

  // 测试设置值
  test('should set count to specific value', () => {
    const { result } = renderHook(() => useCounter(0));
    
    act(() => {
      result.current.setCount(100);
    });
    
    expect(result.current.count).toBe(100);
  });
});
useEffect测试
javascript 复制代码
// src/hooks/__tests__/useFetch.test.js
// 导入React和测试工具
import { renderHook, waitFor } from '@testing-library/react';
// 导入要测试的Hook
import { useFetch } from '../useFetch';

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

describe('useFetch Hook', () => {
  // 在每个测试前清除mock
  beforeEach(() => {
    fetch.mockClear();
  });

  // 测试成功获取数据
  test('should fetch data successfully', async () => {
    const mockData = { id: 1, name: 'Test User' };
    
    // Mock fetch返回成功响应
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockData
    });
    
    const { result } = renderHook(() => useFetch('/api/user/1'));
    
    // 初始状态应该是加载中
    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(null);
    
    // 等待数据加载完成
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
    
    // 断言数据正确
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(null);
  });

  // 测试获取数据失败
  test('should handle fetch error', async () => {
    const errorMessage = 'Failed to fetch';
    
    // Mock fetch返回错误
    fetch.mockRejectedValueOnce(new Error(errorMessage));
    
    const { result } = renderHook(() => useFetch('/api/user/1'));
    
    // 等待错误处理完成
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
    
    // 断言错误状态
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(errorMessage);
  });

  // 测试URL变化时重新获取
  test('should refetch when URL changes', async () => {
    const mockData1 = { id: 1, name: 'User 1' };
    const mockData2 = { id: 2, name: 'User 2' };
    
    fetch
      .mockResolvedValueOnce({
        ok: true,
        json: async () => mockData1
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => mockData2
      });
    
    const { result, rerender } = renderHook(
      ({ url }) => useFetch(url),
      { initialProps: { url: '/api/user/1' } }
    );
    
    // 等待第一次获取完成
    await waitFor(() => {
      expect(result.current.data).toEqual(mockData1);
    });
    
    // 更改URL
    rerender({ url: '/api/user/2' });
    
    // 等待第二次获取完成
    await waitFor(() => {
      expect(result.current.data).toEqual(mockData2);
    });
    
    // 断言fetch被调用了两次
    expect(fetch).toHaveBeenCalledTimes(2);
  });
});

第四部分:高级测试技巧 (1.5小时)

4.1 Mock和Spy

Mock函数示例
javascript 复制代码
// src/utils/__tests__/mock.test.js
// 导入要测试的函数
import { processUserData, sendEmail, calculateTotal } from '../utils';

// Mock外部模块
jest.mock('../api', () => ({
  fetchUser: jest.fn(),
  updateUser: jest.fn()
}));

describe('Mock and Spy examples', () => {
  // 测试Mock函数
  test('should use mock function', () => {
    // 创建Mock函数
    const mockFn = jest.fn();
    
    // 调用Mock函数
    mockFn('arg1', 'arg2');
    
    // 断言Mock函数被调用
    expect(mockFn).toHaveBeenCalled();
    expect(mockFn).toHaveBeenCalledTimes(1);
    expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
  });

  // 测试Mock返回值
  test('should mock function return value', () => {
    const mockFn = jest.fn();
    
    // 设置Mock返回值
    mockFn.mockReturnValue(42);
    
    expect(mockFn()).toBe(42);
    
    // 设置多次调用的不同返回值
    mockFn
      .mockReturnValueOnce(10)
      .mockReturnValueOnce(20)
      .mockReturnValue(30);
    
    expect(mockFn()).toBe(10);
    expect(mockFn()).toBe(20);
    expect(mockFn()).toBe(30);
    expect(mockFn()).toBe(30);
  });

  // 测试Mock Promise
  test('should mock async function', async () => {
    const mockAsyncFn = jest.fn();
    
    // 设置Mock返回Promise
    mockAsyncFn.mockResolvedValue({ data: 'success' });
    
    const result = await mockAsyncFn();
    
    expect(result).toEqual({ data: 'success' });
  });

  // 测试Spy
  test('should spy on object method', () => {
    const obj = {
      method: jest.fn()
    };
    
    // 创建Spy
    const spy = jest.spyOn(obj, 'method');
    
    // 调用方法
    obj.method('test');
    
    // 断言Spy被调用
    expect(spy).toHaveBeenCalledWith('test');
    
    // 恢复原始实现
    spy.mockRestore();
  });

  // 测试模块Mock
  test('should mock module', async () => {
    const { fetchUser } = require('../api');
    
    // 设置Mock返回值
    fetchUser.mockResolvedValue({ id: 1, name: 'Test User' });
    
    const user = await fetchUser(1);
    
    expect(user).toEqual({ id: 1, name: 'Test User' });
    expect(fetchUser).toHaveBeenCalledWith(1);
  });
});

4.2 测试工具函数

测试工具函数
javascript 复制代码
// src/utils/__tests__/testUtils.js
// 测试工具函数集合

// 创建测试包装器
export function createTestWrapper({ providers = [], initialProps = {} } = {}) {
  return function TestWrapper({ children }) {
    return providers.reduce(
      (acc, Provider) => <Provider>{acc}</Provider>,
      children
    );
  };
}

// 等待元素出现
export async function waitForElement(selector, options = {}) {
  const { timeout = 5000, interval = 100 } = options;
  const startTime = Date.now();
  
  while (Date.now() - startTime < timeout) {
    const element = document.querySelector(selector);
    if (element) {
      return element;
    }
    await new Promise(resolve => setTimeout(resolve, interval));
  }
  
  throw new Error(`Element ${selector} not found within ${timeout}ms`);
}

// 模拟用户输入
export async function typeInInput(input, text) {
  const user = userEvent.setup();
  await user.clear(input);
  await user.type(input, text);
}

// 模拟表单提交
export async function submitForm(form) {
  const user = userEvent.setup();
  const submitButton = form.querySelector('button[type="submit"]');
  if (submitButton) {
    await user.click(submitButton);
  }
}

// 创建Mock数据
export function createMockUser(overrides = {}) {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    role: 'user',
    ...overrides
  };
}

export function createMockPost(overrides = {}) {
  return {
    id: 1,
    title: 'Test Post',
    content: 'Test content',
    author: 'Test Author',
    createdAt: new Date().toISOString(),
    ...overrides
  };
}

// 等待异步操作
export function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 测试辅助函数
export const testUtils = {
  waitForElement,
  typeInInput,
  submitForm,
  createMockUser,
  createMockPost,
  wait
};

第五部分:实践项目

项目:完整的测试套件

待办事项组件测试
javascript 复制代码
// src/components/__tests__/TodoApp.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
// 导入用户事件
import userEvent from '@testing-library/user-event';
// 导入要测试的组件
import TodoApp from '../TodoApp';

describe('TodoApp Component', () => {
  // 测试初始渲染
  test('should render todo app with empty list', () => {
    render(<TodoApp />);
    
    // 断言主要元素存在
    expect(screen.getByText('Todo App')).toBeInTheDocument();
    expect(screen.getByPlaceholderText('Add a new todo')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument();
  });

  // 测试添加待办事项
  test('should add new todo item', async () => {
    const user = userEvent.setup();
    
    render(<TodoApp />);
    
    // 获取输入框和按钮
    const input = screen.getByPlaceholderText('Add a new todo');
    const addButton = screen.getByRole('button', { name: 'Add' });
    
    // 输入文本并点击添加
    await user.type(input, 'Buy groceries');
    await user.click(addButton);
    
    // 断言新待办事项已添加
    expect(screen.getByText('Buy groceries')).toBeInTheDocument();
    expect(input).toHaveValue('');
  });

  // 测试完成待办事项
  test('should mark todo as completed', async () => {
    const user = userEvent.setup();
    
    render(<TodoApp />);
    
    // 添加待办事项
    const input = screen.getByPlaceholderText('Add a new todo');
    await user.type(input, 'Test todo');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    // 查找复选框并点击
    const checkbox = screen.getByRole('checkbox');
    await user.click(checkbox);
    
    // 断言待办事项已完成
    expect(checkbox).toBeChecked();
    const todoItem = screen.getByText('Test todo');
    expect(todoItem).toHaveClass('completed');
  });

  // 测试删除待办事项
  test('should delete todo item', async () => {
    const user = userEvent.setup();
    
    render(<TodoApp />);
    
    // 添加待办事项
    const input = screen.getByPlaceholderText('Add a new todo');
    await user.type(input, 'Todo to delete');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    // 查找删除按钮并点击
    const deleteButton = screen.getByRole('button', { name: 'Delete' });
    await user.click(deleteButton);
    
    // 断言待办事项已删除
    expect(screen.queryByText('Todo to delete')).not.toBeInTheDocument();
  });

  // 测试过滤待办事项
  test('should filter todos by status', async () => {
    const user = userEvent.setup();
    
    render(<TodoApp />);
    
    // 添加多个待办事项
    const input = screen.getByPlaceholderText('Add a new todo');
    
    await user.type(input, 'Todo 1');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    await user.type(input, 'Todo 2');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    // 完成第一个待办事项
    const checkboxes = screen.getAllByRole('checkbox');
    await user.click(checkboxes[0]);
    
    // 切换到已完成过滤器
    const filterSelect = screen.getByLabelText('Filter:');
    await user.selectOptions(filterSelect, 'completed');
    
    // 断言只显示已完成的待办事项
    expect(screen.getByText('Todo 1')).toBeInTheDocument();
    expect(screen.queryByText('Todo 2')).not.toBeInTheDocument();
  });

  // 测试空输入验证
  test('should not add empty todo', async () => {
    const user = userEvent.setup();
    
    render(<TodoApp />);
    
    const input = screen.getByPlaceholderText('Add a new todo');
    const addButton = screen.getByRole('button', { name: 'Add' });
    
    // 尝试添加空待办事项
    await user.click(addButton);
    
    // 断言没有添加空待办事项
    const todoItems = screen.queryAllByRole('listitem');
    expect(todoItems).toHaveLength(0);
  });
});
用户管理组件测试
javascript 复制代码
// src/components/__tests__/UserManagement.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, waitFor } from '@testing-library/react';
// 导入用户事件
import userEvent from '@testing-library/user-event';
// 导入Provider
import { Provider } from 'react-redux';
import { store } from '../../store/store';
// 导入要测试的组件
import UserManagement from '../UserManagement';

// 创建测试包装器
const TestWrapper = ({ children }) => (
  <Provider store={store}>
    {children}
  </Provider>
);

describe('UserManagement Component', () => {
  // 测试用户列表渲染
  test('should render user list', async () => {
    render(
      <TestWrapper>
        <UserManagement />
      </TestWrapper>
    );
    
    // 等待用户列表加载
    await waitFor(() => {
      expect(screen.getByText('User Management')).toBeInTheDocument();
    });
  });

  // 测试添加用户
  test('should add new user', async () => {
    const user = userEvent.setup();
    
    render(
      <TestWrapper>
        <UserManagement />
      </TestWrapper>
    );
    
    // 等待组件加载
    await waitFor(() => {
      expect(screen.getByText('Add User')).toBeInTheDocument();
    });
    
    // 点击添加用户按钮
    await user.click(screen.getByText('Add User'));
    
    // 填写表单
    await user.type(screen.getByLabelText('Name:'), 'John Doe');
    await user.type(screen.getByLabelText('Email:'), 'john@example.com');
    
    // 提交表单
    await user.click(screen.getByRole('button', { name: 'Submit' }));
    
    // 等待用户添加完成
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
  });

  // 测试编辑用户
  test('should edit existing user', async () => {
    const user = userEvent.setup();
    
    render(
      <TestWrapper>
        <UserManagement />
      </TestWrapper>
    );
    
    // 等待用户列表加载
    await waitFor(() => {
      expect(screen.getByText('User Management')).toBeInTheDocument();
    });
    
    // 查找编辑按钮并点击
    const editButtons = screen.getAllByRole('button', { name: 'Edit' });
    if (editButtons.length > 0) {
      await user.click(editButtons[0]);
      
      // 修改名称
      const nameInput = screen.getByLabelText('Name:');
      await user.clear(nameInput);
      await user.type(nameInput, 'Updated Name');
      
      // 提交更改
      await user.click(screen.getByRole('button', { name: 'Save' }));
      
      // 等待更新完成
      await waitFor(() => {
        expect(screen.getByText('Updated Name')).toBeInTheDocument();
      });
    }
  });

  // 测试删除用户
  test('should delete user', async () => {
    const user = userEvent.setup();
    
    render(
      <TestWrapper>
        <UserManagement />
      </TestWrapper>
    );
    
    // 等待用户列表加载
    await waitFor(() => {
      expect(screen.getByText('User Management')).toBeInTheDocument();
    });
    
    // 查找删除按钮
    const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
    if (deleteButtons.length > 0) {
      const userToDelete = screen.getAllByText(/User/)[0].textContent;
      
      // 点击删除按钮
      await user.click(deleteButtons[0]);
      
      // 确认删除
      const confirmButton = screen.getByRole('button', { name: 'Confirm' });
      await user.click(confirmButton);
      
      // 等待删除完成
      await waitFor(() => {
        expect(screen.queryByText(userToDelete)).not.toBeInTheDocument();
      });
    }
  });
});
测试配置文件
javascript 复制代码
// package.json测试脚本配置
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:verbose": "jest --verbose",
    "test:update": "jest --updateSnapshot"
  }
}
测试运行脚本
bash 复制代码
# 运行所有测试
npm test

# 运行测试并监视文件变化
npm run test:watch

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行测试并显示详细信息
npm run test:verbose

# 更新快照
npm run test:update

练习题目

基础练习

  1. Jest基础练习
javascript 复制代码
// 练习1:编写数学工具函数的测试
// 实现:加法、减法、乘法、除法函数的测试
// 包含:正常情况、边界情况、错误情况

// 练习2:编写字符串工具函数的测试
// 实现:字符串反转、大小写转换、去空格等函数
// 包含:各种输入情况的测试
  1. React组件测试练习
javascript 复制代码
// 练习3:编写Button组件的完整测试
// 实现:渲染、点击、禁用、样式等测试
// 包含:各种props组合的测试

// 练习4:编写Form组件的完整测试
// 实现:输入、验证、提交、重置等测试
// 包含:表单验证逻辑的测试

进阶练习

  1. Hooks测试练习
javascript 复制代码
// 练习5:编写自定义Hook的完整测试
// 实现:useCounter、useFetch、useLocalStorage等Hook
// 包含:各种使用场景的测试

// 练习6:编写复杂组件的集成测试
// 实现:包含多个子组件、状态管理、API调用的组件
// 包含:完整用户流程的测试
  1. E2E测试练习
javascript 复制代码
// 练习7:使用Cypress或Playwright编写E2E测试
// 实现:完整的用户流程测试
// 包含:登录、数据操作、导航等流程
相关推荐
麦麦在写代码22 分钟前
前端学习1
前端·学习
笨鸟笃行23 分钟前
人工智能备考——2.1.1-2.1.5总结
人工智能·学习
sg_knight26 分钟前
微信小程序中 WebView 组件的使用与应用场景
前端·javascript·微信·微信小程序·小程序·web·weapp
AA陈超42 分钟前
ASC学习笔记0012:查找现有的属性集,如果不存在则断言
笔记·学习
凯子坚持 c1 小时前
生产级 Rust Web 应用架构:使用 Axum 实现模块化设计与健壮的错误处理
前端·架构·rust
IT_陈寒1 小时前
Python 3.12新特性实战:5个让你的代码效率翻倍的隐藏技巧!
前端·人工智能·后端
7***53341 小时前
免费的云原生学习资源,K8s+Docker
学习·云原生·kubernetes
程序员小寒1 小时前
前端高频面试题之Vuex篇
前端·javascript·面试
The_Second_Coming1 小时前
Python 学习笔记:基础篇
运维·笔记·python·学习