Vue 开发者的 React 实战指南:测试篇

news2025/1/13 15:47:23

作为 Vue 开发者,在迁移到 React 开发时,测试策略和方法也需要相应调整。本文将从 Vue 开发者熟悉的角度出发,详细介绍 React 中的测试方法和最佳实践。

测试工具对比

Vue 的测试工具

在 Vue 生态中,我们通常使用:

  • Vue Test Utils:官方的组件测试工具
  • Jest:单元测试框架
  • Cypress:端到端测试工具
// Vue 组件测试示例
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter.vue', () => {
  it('increments count when button is clicked', async () => {
    const wrapper = mount(Counter);

    expect(wrapper.text()).toContain('0');

    await wrapper.find('button').trigger('click');

    expect(wrapper.text()).toContain('1');
  });
});

React 的测试工具

在 React 生态中,我们主要使用:

  • React Testing Library:官方推荐的测试工具
  • Jest:单元测试框架
  • Cypress:端到端测试工具
// React 组件测试示例
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

describe('Counter', () => {
  it('increments count when button is clicked', () => {
    render(<Counter />);

    expect(screen.getByText('0')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button'));

    expect(screen.getByText('1')).toBeInTheDocument();
  });
});

主要区别:

  1. 测试理念
    • Vue Test Utils 偏向实现细节
    • React Testing Library 偏向用户行为
  2. API 设计
    • Vue 使用 wrapper API
    • React 使用 DOM API
  3. 查询方式
    • Vue 可以直接访问组件实例
    • React 推荐使用可访问性查询

组件测试

1. 基础组件测试

// Button.tsx
function Button({ onClick, children, disabled }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className="btn"
    >
      {children}
    </button>
  );
}

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

describe('Button', () => {
  it('renders children correctly', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('handles click events', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('can be disabled', () => {
    const handleClick = jest.fn();
    render(
      <Button onClick={handleClick} disabled>
        Click me
      </Button>
    );

    const button = screen.getByText('Click me');
    expect(button).toBeDisabled();

    fireEvent.click(button);
    expect(handleClick).not.toHaveBeenCalled();
  });
});

2. 表单组件测试

// LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  const mockSubmit = jest.fn();

  beforeEach(() => {
    mockSubmit.mockClear();
  });

  it('validates required fields', async () => {
    render(<LoginForm onSubmit={mockSubmit} />);

    fireEvent.click(screen.getByRole('button', { name: /submit/i }));

    expect(await screen.findByText(/username is required/i)).toBeInTheDocument();
    expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
    expect(mockSubmit).not.toHaveBeenCalled();
  });

  it('validates email format', async () => {
    render(<LoginForm onSubmit={mockSubmit} />);

    await userEvent.type(
      screen.getByLabelText(/email/i),
      'invalid-email'
    );

    fireEvent.click(screen.getByRole('button', { name: /submit/i }));

    expect(await screen.findByText(/invalid email format/i)).toBeInTheDocument();
    expect(mockSubmit).not.toHaveBeenCalled();
  });

  it('submits form with valid data', async () => {
    render(<LoginForm onSubmit={mockSubmit} />);

    await userEvent.type(
      screen.getByLabelText(/email/i),
      'test@example.com'
    );
    await userEvent.type(
      screen.getByLabelText(/password/i),
      'password123'
    );

    fireEvent.click(screen.getByRole('button', { name: /submit/i }));

    await waitFor(() => {
      expect(mockSubmit).toHaveBeenCalledWith({
        email: 'test@example.com',
        password: 'password123'
      });
    });
  });
});

3. 异步组件测试

// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';

const server = setupServer(
  rest.get('/api/user/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com'
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('UserProfile', () => {
  it('displays loading state initially', () => {
    render(<UserProfile userId={1} />);
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('displays user data after successful fetch', async () => {
    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
      expect(screen.getByText('john@example.com')).toBeInTheDocument();
    });
  });

  it('handles error state', async () => {
    server.use(
      rest.get('/api/user/:id', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});

Hook 测试

1. 自定义 Hook 测试

// useCounter.ts
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

// useCounter.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('initializes with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('increments counter', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('decrements counter', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  it('resets counter', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.increment();
      result.current.reset();
    });

    expect(result.current.count).toBe(5);
  });
});

2. Context Hook 测试

// useAuth.test.tsx
import { renderHook, act } from '@testing-library/react-hooks';
import { AuthProvider, useAuth } from './useAuth';

describe('useAuth', () => {
  const wrapper = ({ children }) => (
    <AuthProvider>{children}</AuthProvider>
  );

  it('provides authentication state', () => {
    const { result } = renderHook(() => useAuth(), { wrapper });

    expect(result.current.isAuthenticated).toBe(false);
    expect(result.current.user).toBeNull();
  });

  it('handles login', async () => {
    const { result } = renderHook(() => useAuth(), { wrapper });

    await act(async () => {
      await result.current.login('test@example.com', 'password');
    });

    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user).toEqual({
      email: 'test@example.com'
    });
  });

  it('handles logout', async () => {
    const { result } = renderHook(() => useAuth(), { wrapper });

    await act(async () => {
      await result.current.login('test@example.com', 'password');
      await result.current.logout();
    });

    expect(result.current.isAuthenticated).toBe(false);
    expect(result.current.user).toBeNull();
  });
});

集成测试

1. 路由测试

// App.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import App from './App';

describe('App routing', () => {
  it('navigates to different pages', () => {
    const history = createMemoryHistory();
    render(
      <Router history={history}>
        <App />
      </Router>
    );

    // 首页
    expect(screen.getByText(/welcome/i)).toBeInTheDocument();

    // 导航到关于页
    fireEvent.click(screen.getByText(/about/i));
    expect(screen.getByText(/about us/i)).toBeInTheDocument();

    // 导航到用户页
    fireEvent.click(screen.getByText(/users/i));
    expect(screen.getByText(/user list/i)).toBeInTheDocument();
  });

  it('handles 404 pages', () => {
    const history = createMemoryHistory();
    history.push('/invalid-route');

    render(
      <Router history={history}>
        <App />
      </Router>
    );

    expect(screen.getByText(/404/i)).toBeInTheDocument();
  });
});

2. Redux 集成测试

// store.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
import TodoList from './TodoList';

describe('TodoList with Redux', () => {
  let store;

  beforeEach(() => {
    store = configureStore({
      reducer: rootReducer,
      preloadedState: {
        todos: []
      }
    });
  });

  it('adds new todo', () => {
    render(
      <Provider store={store}>
        <TodoList />
      </Provider>
    );

    const input = screen.getByPlaceholderText(/add todo/i);
    fireEvent.change(input, { target: { value: 'New Todo' } });
    fireEvent.click(screen.getByText(/add/i));

    expect(screen.getByText('New Todo')).toBeInTheDocument();
    expect(store.getState().todos).toHaveLength(1);
  });

  it('toggles todo completion', () => {
    store = configureStore({
      reducer: rootReducer,
      preloadedState: {
        todos: [
          { id: 1, text: 'Test Todo', completed: false }
        ]
      }
    });

    render(
      <Provider store={store}>
        <TodoList />
      </Provider>
    );

    fireEvent.click(screen.getByText('Test Todo'));

    expect(store.getState().todos[0].completed).toBe(true);
  });
});

端到端测试

使用 Cypress

// cypress/integration/auth.spec.js
describe('Authentication', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('successfully logs in', () => {
    cy.get('[data-testid="email-input"]')
      .type('test@example.com');

    cy.get('[data-testid="password-input"]')
      .type('password123');

    cy.get('[data-testid="login-button"]')
      .click();

    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-profile"]')
      .should('contain', 'test@example.com');
  });

  it('displays validation errors', () => {
    cy.get('[data-testid="login-button"]')
      .click();

    cy.get('[data-testid="email-error"]')
      .should('be.visible')
      .and('contain', 'Email is required');

    cy.get('[data-testid="password-error"]')
      .should('be.visible')
      .and('contain', 'Password is required');
  });

  it('handles invalid credentials', () => {
    cy.get('[data-testid="email-input"]')
      .type('wrong@example.com');

    cy.get('[data-testid="password-input"]')
      .type('wrongpassword');

    cy.get('[data-testid="login-button"]')
      .click();

    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Invalid credentials');
  });
});

测试最佳实践

  1. 测试策略

    • 遵循测试金字塔
    • 关注用户行为
    • 避免测试实现细节
    • 保持测试简单
  2. 代码组织

    • 合理组织测试文件
    • 使用测试工具函数
    • 避免重复代码
    • 维护测试数据
  3. 性能考虑

    • 合理使用 mock
    • 避免不必要的等待
    • 优化测试运行时间
    • 并行运行测试

小结

  1. React 测试的特点:

    • 行为驱动测试
    • 可访问性优先
    • 组件化测试
    • 工具链完善
  2. 从 Vue 到 React 的转变:

    • 适应新的测试理念
    • 掌握测试工具
    • 建立测试意识
    • 实践测试策略
  3. 开发建议:

    • 先写测试再实现
    • 保持测试简单
    • 关注测试覆盖
    • 持续维护测试

下一篇文章,我们将深入探讨 React 的部署和持续集成策略,帮助你构建完整的开发流程。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2276027.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[云原生之旅] K8s-Portforward的另类用法, 立省两个端口

前言 此方法适用于Pod不需要大量连接的情况: 有多个pod在执行任务, 偶尔需要连接其中一个pod查看进度/日志;对pod执行一个脚本/命令; 不适用于大量连接建立的情况: pod启的数据库服务;pod启的Api服务;pod启的前端服务;pod启的Oss服务; Portforward简介 Portforward就是端…

MySQL表的增删改查(基础)-下篇

修改 真正在改硬盘了&#xff0c;这样的修改是“持久有效”。一定要确保&#xff0c;update的修改是改对了&#xff0c;改出问题来就麻烦。指定update的时候&#xff0c;如果当前不指定任何条件&#xff0c;就会针对所有的行都能生效&#xff01; (把整个表都给改了)。 案例 --…

Conda虚拟Python环境下安装包遇到的坑

明天下午要去参加Nvidia组织的一个开发者夏令营活动&#xff0c;按照2024 NVIDIA开发者社区夏令营环境配置指南(Win & Mac)_nvidia mac-CSDN博客提供的指引配置环境。里面建议的是用conda来配置Python虚拟环境&#xff0c;原本本机直接安装最直接&#xff0c;不过正好学习下…

【Spring】@Size 无法拦截null的原因

问题复现 在构建 Web 服务时&#xff0c;我们一般都会对一个 HTTP 请求的 Body 内容进行校验&#xff0c;例如我们来看这样一个案例及对应代码。当开发一个学籍管理系统时&#xff0c;我们会提供了一个 API 接口去添加学生的相关信息&#xff0c;其对象定义参考下面的代码&…

Sping Boot教程之五十四:Spring Boot Kafka 生产者示例

Spring Boot Kafka 生产者示例 Spring Boot 是 Java 编程语言中最流行和使用最多的框架之一。它是一个基于微服务的框架&#xff0c;使用 Spring Boot 制作生产就绪的应用程序只需很少的时间。Spring Boot 可以轻松创建独立的、生产级的基于 Spring 的应用程序&#xff0c;您可…

FairGuard游戏安全2024年度报告

导 读&#xff1a;2024年&#xff0c;国内游戏市场实际销售收入3257.83亿元&#xff0c;同比增长7.53%&#xff0c;游戏用户规模6.74亿人&#xff0c;同比增长0.94%&#xff0c;市场收入与用户规模双双实现突破&#xff0c;迎来了历史新高点。但游戏黑灰产规模也在迅速扩大&…

NVIDIA Clara平台助力医学影像处理:编程案例与实践探索(上)

一、引言 1.1 研究背景与意义 在现代医学领域,医学影像技术已然成为疾病诊断、治疗方案制定以及疗效评估的关键支柱。从早期的 X 射线成像,到如今的计算机断层扫描(CT)、磁共振成像(MRI)、正电子发射断层扫描(PET)等先进技术,医学影像为医生提供了直观、精准的人体内…

influxdb 采集node_exporter数据

一、打开Scrapers添加 node_exporter地址&#xff1a;http://192.168.31.135:9100/metrics 查看数据

卷积神经04-TensorFlow环境安装

卷积神经04-TensorFlow环境安装 文章目录 卷积神经04-TensorFlow环境安装0-核心知识脉络1-参考网址2-手动安装Tensorflow-GPU1-关于Tensorflow的官网介绍2-安装Tensorflow-GPU版本1-安装Tensorflow-GPU版本脉络2-安装Tensorflow-GPU版本细节1-Anacanda配置【Python指定版本】-当…

四、智能体强化学习——单智能体工程实践与部署

实验环境与工具模型部署性能指标与评估 搭建强化学习的实验环境、调参与分布式训练&#xff0c;以及将训练好的模型集成到生产系统中并进行监控和评估。 4.1 实验环境与工具 在实际项目中&#xff0c;构建一个稳定、可扩展的实验环境至关重要。以下是一些常用的工具和方法。…

OA项目登录

导入依赖,下面的依赖是在这次OA登录中用到的 <!--web依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.sprin…

电池预测 | 第21讲 基于Gamma伽马模型结合EM算法和粒子滤波算法参数估计的锂电池剩余寿命预测

电池预测 | 第21讲 基于Gamma伽马模型结合EM算法和粒子滤波算法参数估计的锂电池剩余寿命预测 目录 电池预测 | 第21讲 基于Gamma伽马模型结合EM算法和粒子滤波算法参数估计的锂电池剩余寿命预测预测效果基本描述程序设计参考资料 预测效果 基本描述 电池预测 | 第21讲 基于Ga…

实用操作系统学习笔记

第1章 操作系统概述 操作系统基本概念 【基础知识】 操作系统&#xff1a;控制和管理整个计算机系统的硬件和软件资源&#xff0c;合理地组织、调度计算机的工作与资源的分配&#xff0c;进而为用户和其他软件提供方便接口与环境的程序集合。操作系统是计算机系统中最基本的…

银河麒麟桌面操作系统搭建FTP服务器

一、操作环境 服务端&#xff1a;银河麒麟桌面操作系统V10 客户端&#xff1a;银河麒麟桌面操作系统V10 二、服务器配置 说明&#xff1a;以下命令均在终端执行。鼠标点击桌面右键&#xff0c;在终端中打开。 操作步骤&#xff1a; &#xff08;一&#xff09;安装vsftpd…

20250112面试鸭特训营第20天

更多特训营笔记详见个人主页【面试鸭特训营】专栏 250112 1. TCP 和 UDP 有什么区别&#xff1f; 特性TCPUDP连接方式面向连接&#xff08;需要建立连接&#xff09;无连接&#xff08;无需建立连接&#xff09;可靠性可靠的&#xff0c;提供确认、重传机制不可靠&#xff0c…

深入理解 C 语言中浮点型数据在内存中的存储

文章目录 一、浮点型数据存储格式&#xff08;IEEE 754 标准&#xff09;二、举例说明单精度浮点数存储过程三、绘图说明四、双精度浮点数存储示例&#xff08;以1.5为例&#xff09; 在 C 语言的世界里&#xff0c;数据类型丰富多样&#xff0c;而浮点型数据用于表示实数&…

使用uniapp 微信小程序一些好用的插件分享

总结一下自己在开发中遇见的一问题&#xff0c;通过引入组件可以快速的解决 1.zxz-uni-data-select 下拉框选择器(添加下拉框检索&#xff0c;多选功能&#xff0c;多选搜索功能&#xff0c;自定义 下拉框插件&#xff0c;使用这个的原因是因为 uniui uview 组件库下拉框太…

kafka消费堆积问题探索

背景 我们的商城项目用PHP写的&#xff0c;原本写日志方案用的是PHP的方案&#xff0c;但是&#xff0c;这个方案导致资源消耗一直降不下来&#xff0c;使用了20个CPU。后面考虑使用通过kafka的方案写日志&#xff0c;商城中把产生的日志丢到kafka中&#xff0c;在以go写的项目…

使用conda出现requests.exceptions.HTTPError 解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

智能化文档开发(DI)

这个文档涉及到多模态&#xff08;文本、发票、订单、语音&#xff09; 对于普通的文本&#xff0c;我们希望对某些实体的某些属性挖空生成文档模版&#xff0c;并根据预设字段填空最后生成正式文件对于发票、订单&#xff0c;我们想提取它的字段信息&#xff0c;写入DB对于一些…