组件库单元测试和代码覆盖率

568 阅读2分钟

技术选型

主要对比jest和vitest。jest在测试框架领域占据了主导地位,而vitest是对vite项目匹配度更好的测试框架。
vitest官方给出的测试框架对比:cn.vitest.dev/guide/compa…
最终选择vitest的原因:

  1. 项目是vite,引入vitest可以复用原有的一些配置、插件
  2. 内置的 TypeScript / JSX 支持,项目中使用了ts,使用vitest就天然支持,而jest需要再引入ts-jest并配置
  3. vitest主打极速
  4. 在pnpm test模式下,支持热更新,实时调试
  5. vitest的限制(比如:esm优先,不支持cjs和esm混合使用),对项目不会有影响

配置

项目简介

当前项目结构目录如下:

.
└── packages
    ├── components #组件
    │   └── src
    │       ├── style
    │       └── ui-button
    └── site #文档
        └── docs

项目是monorepo,使用pnpm-workspace实现,现在只考虑对其中一个包(UI组件库包:packages/components)进行单元测试。首先需要在根目录下新增[vitest.workspace.ts](https://cn.vitest.dev/guide/workspace.html)

export default ["packages/*"];

安装依赖

除了vitest外,还需安装以下插件:
happy-dom用于 DOM 模拟
@vue/test-utils简化对vue组件的测试
@vitest/coverage-v8 在v8基础上封装的测试覆盖率

$ pnpm add vitest happy-dom @vue/test-utils @vitest/coverage-v8 --filter lylaa-ui -D

为了在测试脚本中使用jsx/tsx语法(不习惯使用vue3的渲染函数创建组件),安装@vitejs/plugin-vue-jsx

pnpm add @vitejs/plugin-vue-jsx --filter lylaa-ui -D

配置文件

Vite 配置中添加 test 属性,在配置文件的顶部使用 三斜杠指令,用于声明对vitest的依赖

/// <reference types="vitest" />
...
import vueJsx from "@vitejs/plugin-vue-jsx";
...

export default defineConfig({
	...
 	plugins: [... vueJsx()],
  test: {
    environment: "happy-dom",
  },
})

ts支持jsx语法

{
  "compilerOptions": {
    "baseUrl": ".",
    "jsx": "preserve",
    "strict": false,
    "target": "ESNext",
    "module": "ESNext",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "lib": ["esnext", "dom"]
  },
  "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "src/**/*.tsx"], // 该行增加“src/**/*.tsx”
  "references": [{ "path": "./tsconfig.node.json" }]
}

新增命令

"scripts": {
   ...
    "test": "pnpm run --filter lylaa-ui test",
    "coverage": "pnpm run --filter lylaa-ui coverage"
}
"scripts": {
    ...
    "test": "vitest",
    "coverage": "vitest run --coverage"
}

编写测试脚本

创建测试脚本

  1. 在对应的组件文件夹下新建__tests__文件夹
  2. 在文件夹中新建测试脚本文件index.test.tsx
  3. 使用toMatchSnapshot生成组件快照,测试在第一次运行时,Vitest 会在测试脚本同级创建一个快照文件夹,在随后的测试运行中,Vitest 会将执行的输出与之前的快照进行比较。如果他们匹配,测试就会通过。如果它们不匹配,要么测试运行时在你的代码中发现了应该修复的错误,要么实现已经更改,需要更新快照。

以按钮为例,编写一个简单的单元测试脚本:

import { describe, expect, it, vi } from "vitest";
import { mount } from "@vue/test-utils";
import UiButton from "../UiButton.vue";

describe("Button", () => {
  it("按钮渲染", () => {
    const wrapper = mount({
      render() {
        return <UiButton>hello Lylaa ui</UiButton>;
      },
    });
    // 生成快照
    expect(wrapper.html()).toMatchSnapshot();
    expect(wrapper.text()).toBe("hello Lylaa ui");
  });
  it("按钮点击", () => {	
    // 模拟回调
    const onClick = vi.fn();
    const wrapper = mount({
      render() {
        return <UiButton onClick={onClick}>按钮点击</UiButton>;
      },
    });
    // 模拟事件
    wrapper.trigger("click");
    expect(onClick).toHaveBeenCalledWith();
  });
  it("按钮禁用不可点击", () => {
    const onClick = vi.fn();
    const wrapper = mount({
      render() {
        return (
          <UiButton disabled={true} onClick={onClick}>
          按钮点击
          </UiButton>
        );
      },
    });

    expect(wrapper.html()).toMatchSnapshot();
    wrapper.trigger("click");
    expect(onClick).not.toHaveBeenCalledWith();
  });
});

代码测试覆盖率及可视化报告

测试覆盖率是衡量测试质量的主要标准之一,含义是当前的测试对于源代码的执行覆盖程度。

安装插件

安装@vitest/coverage-v8(测试覆盖率)和@vitest/ui(提供可视化交互界面)

$ pnpm add @vitest/coverage-v8 @vitest/ui --filter lylaa-ui -D

配置

export default defineConfig({
  ...
  test: {
    ...
    coverage: {
      provider: "v8", // 可选v8 和 istanbul
      enabled: true, // Vitest UI 将启用覆盖率报告
    },
    reporters: ["default", "html"], 
  	// default: 在终端中实时查看测试的运行情况
  	// html: 使用 'html' 报告器生成 HTML 输出并预览测试结果
  },
});

配置相关命令:

{
  ...
  "scripts": {
    ...
    "coverage": "vitest run --coverage",
    "report": "vitest --ui"
  },
}
{
  ...
  "scripts": {
    ...
    "coverage": "pnpm run --filter lylaa-ui coverage",
    "report": "pnpm run --filter lylaa-ui report"
  },
}

运行

查看测试覆盖率,跑命令pnpm coverage,输出以下结果:
image.png
查看测试报告,跑pnpm report
image.png
可在线修改测试脚本和运行,此外还可查看测试覆盖率报告,点击左侧边栏顶部按钮
image.png
image.png

示例代码

github.com/Elemy/lylaa…

问题记录

  1. monorepo中的某一个包进行单元测试时,需要把依赖安装在对应包下,不能安装为共享依赖
  2. tooltips组件使用trigger触发mouseenter,通不过测试
    1. 原测试脚本:
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";
import UiTooltip from "../UiTooltip.vue";

describe("Tooltip", () => {
  it("鼠标移入", async () => {
    const wrapper = mount({
      render() {
        return (
          <UiTooltip title="hello lyla ui">
            <div id="hello" class="hello-class">
              hello Lylaa ui
            </div>
          </UiTooltip>
        );
      },
    });
    expect(wrapper.html()).toMatchSnapshot();
    await wrapper.find("#hello").trigger("mouseenter");
    expect(wrapper.find("#hello").classes()).toContain("testui-tooltip-open");
  });
});

antdv中的tooltip,鼠标移入后,会延迟展示tips,默认延迟100ms,可以设置定时器延迟执行断言语句。

  1. 修改后:
describe("Button", () => {
  it("鼠标移入", async () => {
    ...
    await wrapper.find("#hello").trigger("mouseenter");
    await new Promise((resolve) => setTimeout(resolve, 200)); # 新增该行
    expect(wrapper.find("#hello").classes()).toContain("testui-tooltip-open");
  });
});