Element Plus 组件库实现:Button——Vitest测试

Vitest

看标题,没错,本次测试用到的是Vitest,为什么选择Vitest而不是Jest,首先,如果看Vitest的文档是可以看到Vitest是兼容Jest的API,另外一个重要的原因就是,Vite和Vitest本来就可以看作一家人,只是分工不同,官方大大是这样说的:Vitest 旨在将自己定位为 Vite 项目的首选测试框架,即使对于不使用 Vite 的项目也是一个可靠的替代方案

为什么要测试

你可能会问,为什么我们要花时间和精力去学习和编写测试呢?尤其是在接触测试的时候,确实会让人感到复杂且难以掌握。毕竟,直接引入组件并手动测试,或者通过控制台打印来调试工具函数,似乎更简单直接。那么,为什么我们还需要专门学习如何编写自动化测试呢?

1. 随着项目规模的增长,手动测试变得不切实际

当你刚开始开发一个简单的组件时,比如 Button.vue,属性不多,功能也不复杂,手动测试可能确实是一个可行的选择。你可以逐个检查每个属性的行为,确保一切正常。但是,随着项目的不断扩展,组件的属性越来越多,功能也变得更加复杂。此时,手动测试的效率会大大降低,甚至可能出现遗漏的情况。

  • 新增属性:每次新增一个属性或功能时,都需要重新手动测试所有相关场景,确保新功能不会影响现有功能。
  • 代码更新:当你对代码进行优化、修复 bug 或者重构时,手动测试无法保证所有的旧功能仍然正常工作。你可能会不小心引入新的问题,而这些问题在手动测试中很容易被忽略。
  • 团队协作:在一个多人协作的项目中,不同的开发者可能会修改同一个组件。如果没有自动化测试,很难确保每个人的修改都不会破坏其他人的工作。

2. 自动化测试提高了开发效率

自动化测试可以大幅提高开发效率。通过编写测试用例,你可以让计算机自动执行这些测试,而不需要每次都手动点击、输入和检查结果。这不仅节省了时间,还能确保每次代码更改后,所有功能都能正常工作。

  • 快速反馈:每次提交代码后,自动化测试可以在几秒钟内告诉你是否有任何问题。这样你可以在早期发现问题,避免后期调试的麻烦。
  • 减少回归问题:自动化测试可以帮助你检测到代码更新后是否引入了新的问题,防止回归(即修复了一个问题,却引入了另一个问题)。
  • 增强信心:有了可靠的测试覆盖率,开发者可以在修改代码时更加自信,而不必担心会破坏现有的功能。

3. 测试是代码质量的保障

编写测试不仅是为了解决当前的问题,更是为了确保代码的长期可维护性和稳定性。良好的测试覆盖率可以让你的代码更具健壮性,减少潜在的错误和漏洞。

  • 文档化行为:测试用例实际上是对组件行为的一种文档化。通过阅读测试代码,其他开发者可以快速理解组件的功能和预期行为。
  • 捕获边缘情况:手动测试往往只能覆盖常见的使用场景,而自动化测试可以帮助你捕获一些边缘情况和异常情况,确保组件在各种情况下都能正常工作。
  • 持续集成(CI)的支持:自动化测试可以轻松集成到持续集成(CI)管道中,确保每次代码提交后都能自动运行测试,进一步提高代码的质量和可靠性。

4. 测试帮助你更好地设计代码

编写测试的过程不仅仅是验证代码是否正确,它还可以帮助你更好地设计代码。通过编写测试,你可以更清晰地思考组件的接口、行为和边界条件。这种"测试驱动开发"(TDD)的方法可以让你写出更加简洁、易于维护的代码。

  • 模块化设计:为了方便测试,你会倾向于将代码分解成更小的、独立的模块。这不仅有助于测试,还能提高代码的复用性和可维护性。
  • 清晰的接口:编写测试时,你需要明确组件的输入和输出。这有助于你设计出更加清晰、易用的 API。
  • 提前发现设计问题:在编写测试的过程中,你可能会发现一些设计上的问题,比如某些功能难以测试,或者某些逻辑过于复杂。这些问题可以通过调整设计来解决,从而提高代码的整体质量。

测试Button组件

下载Vitest

npm install -D vitest

我们这里使用的是Vue,所以还要下载基于Vue的测试工具,显然Vitest肯定不是只能测试基于Vite构建或使用Vue框架相关的项目

下载vue-test-utils

bash 复制代码
npm install -D @vue/test-utils

Vitest 配置

安装完 Vitest 后,根文件夹中添加 vitest.config.js 文件中:

perl 复制代码
/// <reference types="vitest" />
// 上面这行是 TypeScript 的三斜杠指令,用于引用 `vitest` 的类型定义。它确保我们在编写测试时可以使用 Vitest 提供的全局 API(如 `describe`、`test`、`expect` 等),并且获得正确的类型提示。

import { defineConfig } from "vite";
// 从 Vite 中导入 `defineConfig` 函数,用于定义 Vite 的配置对象。Vite 是一个现代的构建工具,支持快速开发和高效的生产构建。

import vue from "@vitejs/plugin-vue";
// 导入 Vite 的 Vue 插件,该插件用于处理 `.vue` 文件,支持单文件组件(SFC)的编译和热更新。

import vueJsx from "@vitejs/plugin-vue-jsx";
// 导入 Vite 的 Vue JSX 插件,该插件允许在 Vue 组件中使用 JSX 语法,适用于需要 JSX 支持的项目。

import VueMacros from "unplugin-vue-macros";
// 导入 `unplugin-vue-macros`,这是一个增强 Vue 开发体验的插件。它提供了多个宏(如 `defineComponent`、`setup` 等),简化了 Vue 组件的编写,并且与其他工具(如 Vite 和 Vue-Test-Utils)兼容。

// Vite 配置对象,用于定义项目的构建和开发环境设置
export default defineConfig({
  plugins: [
    // 使用 `VueMacros.vite` 来配置 Vite 插件链。`VueMacros` 是一个元插件,它可以自动加载并配置其他插件(如 `vue` 和 `vueJsx`),并且提供额外的功能。
    VueMacros.vite({
      plugins: {
        // 配置 Vue 插件,处理 `.vue` 文件
        vue: vue(),
        // 配置 Vue JSX 插件,支持 JSX 语法
        vueJsx: vueJsx(),
      },
    }),
  ],
  test: {
    globals: true, // 启用全局模式,使得 Vitest 的全局 API(如 `describe`、`test`、`expect` 等)可以直接在测试文件中使用,而不需要显式导入。
    environment: "jsdom", // 指定测试环境为 `jsdom`,这是一个虚拟的 DOM 实现,适用于浏览器环境的测试。`jsdom` 可以模拟浏览器的行为,使得我们可以测试与 DOM 相关的功能,而不需要实际打开浏览器。
  },
});

开始测试

php 复制代码
// Button.test.ts

import { describe, test, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Button from "./Button.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Icon from "../Icon/Icon.vue";

// 使用 vitest 的 describe 函数来定义测试套件,测试 Button.vue 组件
describe("Button.vue", () => {
  // 测试基础按钮的功能
  test("basic button", () => {
    // 使用 mount 函数挂载 Button 组件,并传递 props 和 slots
    const wrapper = mount(Button, {
      props: {
        type: "primary", // 设置按钮类型为 "primary"
      },
      slots: {
        default: "button", // 设置默认插槽内容为 "button"
      },
    });

    // 打印组件的 HTML 结构,用于调试
    console.log(wrapper.html());

    // 检查按钮是否具有预期的类名 "jd-button--primary"
    expect(wrapper.classes()).toContain("jd-button--primary");

    // 检查按钮的文本内容是否为 "button"
    expect(wrapper.get("button").text()).toBe("button");

    // 检查页面中只有一个按钮元素
    expect(wrapper.findAll("button").length).toBe(1);

    // 触发按钮的点击事件
    wrapper.get("button").trigger("click");

    // 检查按钮是否触发了 "click" 事件
    expect(wrapper.emitted()).toHaveProperty("click");
  });

  // 测试禁用按钮的功能
  test("disabled button", () => {
    // 挂载 Button 组件,并设置 disabled 属性为 true
    const wrapper = mount(Button, {
      props: {
        disabled: true,
      },
      slots: {
        default: "button",
      },
    });

    // 检查按钮是否具有 "disabled" 属性
    expect(wrapper.get("button").attributes("disabled")).toBeDefined();

    // 触发禁用按钮的点击事件
    wrapper.get("button").trigger("click");

    // 检查禁用按钮是否没有触发 "click" 事件
    expect(wrapper.emitted()).not.toHaveProperty("click");
  });

  // 测试带有图标的按钮
  test("icon", () => {
    // 挂载 Button 组件,并设置 icon 属性为 "search"
    const wrapper = mount(Button, {
      props: {
        icon: "search",
      },
      slots: {
        default: "button",
      },
      global: {
        stubs: ["FontAwesomeIcon"], // 模拟第三方组件 FontAwesomeIcon
      },
    });

    // 检查按钮内部是否存在 FontAwesomeIcon 组件
    const icon = wrapper.findComponent(FontAwesomeIcon);
    expect(icon.exists()).toBe(true);

    // 检查图标组件的 icon 属性是否为 "search"
    expect(icon.props("icon")).toBe("search");
  });

  // 测试加载中的按钮
  test("loading", () => {
    // 挂载 Button 组件,并设置 loading 属性为 true
    const wrapper = mount(Button, {
      props: {
        loading: true,
      },
      slots: {
        default: "loading",
      },
      global: {
        stubs: ["Icon"], // 模拟本地的 Icon 组件
      },
    });

    // 打印组件的 HTML 结构,用于调试
    console.log(wrapper.html());

    // 检查按钮是否具有 "is-loading" 类名
    expect(wrapper.get("button").classes()).toContain("is-loading");

    // 检查按钮的文本内容是否为 "loading"
    expect(wrapper.get("button").text()).toBe("loading");

    // 检查按钮内部是否存在 Icon 组件
    const icon = wrapper.findComponent(Icon);
    expect(icon.exists()).toBe(true);

    // 检查图标组件的 icon 属性是否为 "spinner"
    expect(icon.props("icon")).toBe("spinner");

    // 检查按钮是否具有 "disabled" 属性
    expect(wrapper.attributes("disabled")).toBeDefined();
  });
});

启动测试,Button就是测试代码所在的文件名 => Button.test.ts

css 复制代码
npx vitest Button

测试结果如下:

可以看到生成了button节点且测试内容均生效

至于测试中使用到的函数及方法,这里简单介绍下:

  1. describe:

    • describe 是一个函数,用于对一组相关的测试用例进行分组。它接受一个字符串作为描述(通常描述这个测试组是关于什么功能的),以及一个回调函数,该回调函数包含与该描述相关的 test 函数。
    • 使用 describe 可以使测试结构更清晰,特别是当你有大量的测试用例时。
  2. test:

    • test(有时也被称为 it,取决于测试框架,Vites兼容了Jest,所以写it也可以)是定义单个测试用例的函数。它接受一个字符串作为测试用例的描述,以及一个执行测试逻辑的回调函数。
    • 在这个回调函数中,你会使用 expect 函数来断言某些条件是否满足,从而验证代码的正确性。
  3. expect:

    • expect 是一个函数,用于声明一个"期望"或"断言"。它接受一个值,并允许你链式调用各种"匹配器"(matchers)来检查该值是否满足某些条件。
    • 例如,你可以使用 expect(someValue).toBe(anotherValue) 来检查 someValue 是否等于 anotherValue
  4. mount:

    • 函数是 @vue/test-utils 中的一个核心功能,它用于模拟 Vue 组件的挂载过程,并返回一个包装器对象(Wrapper),这个对象提供了一系列的方法和属性,用于访问和操作组件实例及其子组件。

测试的重要性

所以看到这里应该知道测试的重要性了:

  • 自动化完成流程,保证代码的运行结果
  • 更早发现 Bug
  • 重构和升级更加容易和可靠

总结

使用 Vitest 对我们的应用程序进行单元测试是无缝的,与Jest等替代品相比,需要更少的步骤来启动和运行。Vitest 还可以很容易地将现有的测试从 Jest 迁移到Vitest,而不需要进行额外的配置。

相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240259 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar9 小时前
纯前端实现更新检测
开发语言·前端·javascript