基于vite创建的react18项目的单元测试

📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)

📝 职场经验干货:

软件测试工程师简历上如何编写个人信息(一周8个面试)

软件测试工程师简历上如何编写专业技能(一周8个面试)

软件测试工程师简历上如何编写项目经验(一周8个面试)

软件测试工程师简历上如何编写个人荣誉(一周8个面试)

软件测试行情分享(这些都不了解就别贸然冲了.)

软件测试面试重点,搞清楚这些轻松拿到年薪30W+

软件测试面试刷题小程序免费使用(永久使用)


最近一个小伙伴进了字节外包,第一个活就是让他写一个单元测试。

嗯,说实话,在今天之前我只知道一些理论,但是并没有实操过,于是我就试验了一下。

通过查询资料,大拿们基本都说基于vite的项目,用vitest进行测试比较方便一些。

闲话不多说,步入正题。

01 下载依赖

在vscode终端输入以下命令:

npm install --save-dev vitest @testing-library/react @testing-library/jest-dom

--save-dev: 

这个标志表示将这些包添加为开发依赖(devDependencies)。这些依赖只在开发环境中使用,而不会被包含在生产环境中。

例如,测试框架和工具通常只在开发时需要,而不需要在生产环境中。

vitest:

这是一个快速的单元测试框架,类似于 Jest,但专为 Vite 生态系统设计。

它支持现代 JavaScript 特性,并且与 Vite 无缝集成,非常适合用于测试 Vite 创建的项目。

@testing-library/react:

这是一个用于测试 React 组件的库,提供了一组 API,使得编写测试变得简单而直观。

它鼓励以用户的方式来测试组件,而不是实现细节,从而提高测试的可靠性和可维护性。

@testing-library/jest-dom: 

这是一个为 Jest 提供的自定义匹配器库,增强了 Jest 的断言功能,使得你可以使用更自然的语法来进行 DOM 相关的断言。

例如,你可以使用 

toBeInTheDocument() 来检查某个元素是否在文档中,而不需要写复杂的查询逻辑。

02 创建testSetup.js文件

文件里只有一行代码:

import '@testing-library/jest-dom';

03 配置vite.config.js文件

代码如下:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './testSetup.js',
  },
})
  • test: 这是 Vitest 的配置部分。

  • globals: true: 这个选项表示在测试文件中可以使用全局的测试函数,比如 describe、test、expect 等,而不需要每次都导入它们。

  • environment: 'jsdom': 这个选项指定测试运行在 jsdom 环境中。jsdom 是一个 JavaScript 实现的 DOM,用于模拟浏览器环境,这样你可以在 Node.js 中运行测试并且测试涉及 DOM 操作的代码。

  • setupFiles: './testSetup.js': 这个选项指定一个设置文件,在测试运行之前会执行。在第二步中我只是引入了一个包。

04 添加脚本

在项目的 package.json 中添加测试脚本:​​​​​​​

"scripts": {
  "test": "vitest"
}

05 创建测试文件

在你的组件目录下,创建一个与组件同名的测试文件,通常以 .test.tsx 结尾。例如,如果你有一个 Wjllogin.jsx 组件,你可以在同一目录下创建 Wjllogin.test.jsx。

我的demo中的Wjllogin.jsx中的代码如下:​​​​​​​

import React, { useState } from "react"; // 导入 React 和 useState Hook
import "./wjs.scss"; // 导入样式文件
import "animate.css"; // 导入动画效果库
import { wjllogin } from "../axiosAPI/wjl"; // 导入用于登录的 API 函数
import { useNavigate } from "react-router-dom"; // 导入路由导航 Hook
import { message } from "antd"; // 导入 Ant Design 的消息提示组件
 
// 定义 Wjllogin 组件
export default function Wjllogin() {
  let navigate = useNavigate(); // 初始化路由导航
  let [name, setName] = useState(""); // 定义状态变量 name 和更新函数 setName
  let [card, setCard] = useState(""); // 定义状态变量 card 和更新函数 setCard
  let [tid, setTid] = useState(""); // 定义状态变量 tid 和更新函数 setTid
 
  const [messageApi, contextHolder] = message.useMessage(); // 使用 Ant Design 的消息提示 API
 
  // 定义登录函数
  let login = async () => {
    // 调用 wjllogin API 进行登录
    let {
      data: { code, sid, clazz, sname }, // 解构 API 返回的数据
    } = await wjllogin({ name, card, tid }); // 传递姓名、身份证号和学号到 API
 
    // 检查返回的状态码
    if (code === 200) {
      // 登录成功,保存用户信息到 sessionStorage
      sessionStorage.setItem("sid", sid); // 保存会话 ID
      sessionStorage.setItem("clazz", clazz); // 保存班级信息
      sessionStorage.setItem("token", "token"); // 保存 token(这里是示例,实际应从 API 获取)
      sessionStorage.setItem("sname", sname); // 保存姓名
 
      // 显示成功消息
      messageApi.open({
        type: "success",
        content: "登录成功, 即将跳转至主页",
      });
 
      // 设置延迟后跳转到主页
      setTimeout(() => {
        navigate("/wjlhome"); // 跳转到主页
      }, 2000);
    } else {
      // 登录失败,显示错误消息
      messageApi.open({
        type: "error",
        content: "登录失败,请检查姓名、身份证号或学号",
      });
    }
  };
 
  // 组件的 JSX 结构
  return (
    <div className="examlogin animate__animated animate__slideInLeft">
      {" "}
      {/* 主容器,包含动画效果 */}
      <div className="top">
        {" "}
        {/* 顶部区域 */}
        <img
          className="main"
          src="https://cdn7.axureshop.com/demo/2001850/images/%E5%9C%A8%E7%BA%BF%E8%80%83%E8%AF%95/u2853.svg"
          alt=""
        />{" "}
        {/* 主图标 */}
        <img
          className="x"
          src="https://cdn7.axureshop.com/demo/2001850/images/%E5%9C%A8%E7%BA%BF%E8%80%83%E8%AF%95/u2854.svg"
          alt=""
        />{" "}
        {/* 副图标 */}
      </div>
      <div className="title">
        {" "}
        {/* 标题区域 */}
        培训学院在线考试系统 {/* 系统名称 */}
        <span className="lessfont">考生版</span> {/* 子标题 */}
      </div>
      <div className="form">
        {" "}
        {/* 表单区域 */}
        <p>
          <label htmlFor="name">考生姓名:</label>
          <input
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </p>
        <p>
          <label htmlFor="card">身份证号:</label>
          <input
            id="card"
            type="text" // 输入框类型
            value={card} // 绑定到 card 状态
            onChange={(e) => {
              setCard(e.target.value); // 更新 card 状态
            }}
          />
        </p>
        <p>
          <label htmlFor="tid">学号:</label> {/* 学号标签 */}
          <input
            id="tid"
            type="text" // 输入框类型
            value={tid} // 绑定到 tid 状态
            onChange={(e) => {
              setTid(e.target.value); // 更新 tid 状态
            }}
          />
        </p>
        <p>
          <button
            className="btn" // 按钮样式
            onClick={() => {
              login(); // 点击按钮时调用 login 函数
            }}>
            登录 {/* 按钮文本 */}
          </button>
        </p>
        {contextHolder} {/* 显示消息提示的容器 */}
      </div>
    </div>
  );
}

测试代码Wjllogin.test.jsx中的代码如下:​​​​​​​

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; // 导入 MemoryRouter
import Wjllogin from './Wjllogin'; // 根据你的文件路径调整
import { wjllogin } from '../axiosAPI/wjl'; // 导入登录 API
import { describe, it, beforeEach, vi } from 'vitest'; // 导入 Vitest 的函数
 
// Mock the API call
vi.mock('../axiosAPI/wjl', () => ({
  wjllogin: vi.fn(),
}));
 
describe('Wjllogin Component', () => {
  beforeEach(() => {
    // 清除所有的 mocks
    vi.clearAllMocks();
  });
 
  it('renders Wjllogin component', () => {
    render(
      <MemoryRouter>
        <Wjllogin />
      </MemoryRouter>
    );
    
    // Check if elements are rendered
    expect(screen.getByText(/考生姓名:/)).toBeInTheDocument();
    expect(screen.getByText(/身份证号:/)).toBeInTheDocument();
    expect(screen.getByText(/学号:/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /登录/i })).toBeInTheDocument();
  });
 
  it('successful login', async () => {
    // Mock the API response for a successful login
    wjllogin.mockResolvedValueOnce({
      data: { code: 200, sid: '123', clazz: 'A1', sname: 'John Doe' },
    });
 
    render(
      <MemoryRouter>
        <Wjllogin />
      </MemoryRouter>
    );
 
    // Fill in the input fields
    fireEvent.change(screen.getByLabelText(/考生姓名:/), { target: { value: 'John Doe' } });
    fireEvent.change(screen.getByLabelText(/身份证号:/), { target: { value: '123456789012345678' } });
    fireEvent.change(screen.getByLabelText(/学号:/), { target: { value: '2023001' } });
 
    // Click the login button
    fireEvent.click(screen.getByRole('button', { name: /登录/i }));
 
    // Wait for the success message to appear
    await waitFor(() => {
      expect(screen.getByText(/登录成功, 即将跳转至主页/)).toBeInTheDocument();
    });
 
    // Check if sessionStorage is set (you may need to mock sessionStorage)
    expect(sessionStorage.getItem('sid')).toBe('123');
    expect(sessionStorage.getItem('clazz')).toBe('A1');
    expect(sessionStorage.getItem('sname')).toBe('John Doe');
  });
 
  it('failed login', async () => {
    // Mock the API response for a failed login
    wjllogin.mockResolvedValueOnce({
      data: { code: 400 },
    });
 
    render(
      <MemoryRouter>
        <Wjllogin />
      </MemoryRouter>
    );
 
    // Fill in the input fields
    fireEvent.change(screen.getByLabelText(/考生姓名:/), { target: { value: 'Invalid User' } });
    fireEvent.change(screen.getByLabelText(/身份证号:/), { target: { value: '000000000000000000' } });
    fireEvent.change(screen.getByLabelText(/学号:/), { target: { value: '0000000' } });
 
    // Click the login button
    fireEvent.click(screen.getByRole('button', { name: /登录/i }));
 
    // Wait for the error message to appear
    await waitFor(() => {
      expect(screen.getByText(/登录失败,请检查姓名、身份证号或学号/)).toBeInTheDocument();
    });
  });
});

解释代码​​​​​​​

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; // 导入 MemoryRouter
import Wjllogin from './Wjllogin'; // 根据你的文件路径调整
import { wjllogin } from '../axiosAPI/wjl'; // 导入登录 API
import { describe, it, beforeEach, vi } from 'vitest'; // 导入 Vitest 的函数
  • @testing-library/react: 这个库提供了一些函数,用于渲染组件和进行交互测试。

  • MemoryRouter: 这是 React Router 提供的一个组件,用于在测试中模拟路由环境。

  • Wjllogin: 这是被测试的组件,假设它是一个登录表单。

  • wjllogin: 这是一个从 API 模块导入的函数,用于处理登录请求。

  • describe, it, beforeEach, vi: 这些是 Vitest 提供的函数,用于组织测试用例和创建 mock 函数。​​​​​​​

vi.mock('../axiosAPI/wjl', () => ({
  wjllogin: vi.fn(),
}));
  • 这里使用 vi.mock 来模拟 wjllogin 函数,以便在测试中控制其返回值,而不实际调用 API。​​​​​​​

describe('Wjllogin Component', () => {
 
}
  • describe 用于将相关的测试用例组织在一起,便于管理和阅读。​​​​​​​

beforeEach(() => {
  vi.clearAllMocks();
});
  • beforeEach 在每个测试用例执行之前调用,确保每个测试用例都在干净的状态下运行,避免测试之间的相互影响。

测试用例​​​​​​​

it('renders Wjllogin component', () => {
  render(
    <MemoryRouter>
      <Wjllogin />
    </MemoryRouter>
  );
 
  // Check if elements are rendered
  expect(screen.getByText(/考生姓名:/)).toBeInTheDocument();
  expect(screen.getByText(/身份证号:/)).toBeInTheDocument();
  expect(screen.getByText(/学号:/)).toBeInTheDocument();
  expect(screen.getByRole('button', { name: /登录/i })).toBeInTheDocument();
});
  • 这个测试用例检查 Wjllogin 组件是否能够正确渲染,并且确保特定的文本和登录按钮存在于文档中。​​​​​​​

it('successful login', async () => {
  // Mock the API response for a successful login
  wjllogin.mockResolvedValueOnce({
    data: { code: 200, sid: '123', clazz: 'A1', sname: 'John Doe' },
  });
 
  render(
    <MemoryRouter>
      <Wjllogin />
    </MemoryRouter>
  );
 
  // Fill in the input fields
  fireEvent.change(screen.getByLabelText(/考生姓名:/), { target: { value: 'John Doe' } });
  fireEvent.change(screen.getByLabelText(/身份证号:/), { target: { value: '123456789012345678' } });
  fireEvent.change(screen.getByLabelText(/学号:/), { target: { value: '2023001' } });
 
  // Click the login button
  fireEvent.click(screen.getByRole('button', { name: /登录/i }));
 
  // Wait for the success message to appear
  await waitFor(() => {
    expect(screen.getByText(/登录成功, 即将跳转至主页/)).toBeInTheDocument();
  });
 
  // Check if sessionStorage is set (you may need to mock sessionStorage)
  expect(sessionStorage.getItem('sid')).toBe('123');
  expect(sessionStorage.getItem('clazz')).toBe('A1');
  expect(sessionStorage.getItem('sname')).toBe('John Doe');
});
  • 这个测试用例模拟了一个成功的登录过程。它首先设置了 wjllogin 函数的返回值,然后渲染组件,填写表单,点击登录按钮,并最终检查成功消息是否出现以及 sessionStorage 是否正确设置。​​​​​​​

it('failed login', async () => {
  // Mock the API response for a failed login
  wjllogin.mockResolvedValueOnce({
    data: { code: 400 },
  });
 
  render(
    <MemoryRouter>
      <Wjllogin />
    </MemoryRouter>
  );
 
  // Fill in the input fields
  fireEvent.change(screen.getByLabelText(/考生姓名:/), { target: { value: 'Invalid User' } });
  fireEvent.change(screen.getByLabelText(/身份证号:/), { target: { value: '000000000000000000' } });
  fireEvent.change(screen.getByLabelText(/学号:/), { target: { value: '0000000' } });
 
  // Click the login button
  fireEvent.click(screen.getByRole('button', { name: /登录/i }));
 
  // Wait for the error message to appear
  await waitFor(() => {
    expect(screen.getByText(/登录失败,请检查姓名、身份证号或学号/)).toBeInTheDocument();
  });
});
  • 这个测试用例模拟了一个失败的登录过程。它设置了 wjllogin 函数的返回值为一个错误代码,然后填写不正确的表单数据,点击登录按钮,并最终检查错误消息是否出现。

这段代码为 Wjllogin 组件提供了全面的测试,包括组件的渲染、成功登录和失败登录的场景。通过使用 Vitest 和 React Testing Library,测试用例能够模拟用户交互并验证组件的行为是否符合预期。

06 运行测试

通过在终端运行命令 npx vitser 看结果

npx vitest

这里三个测试用例全部通过,说明代码编写没有问题。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值