技术选型
主要对比jest和vitest。jest在测试框架领域占据了主导地位,而vitest是对vite项目匹配度更好的测试框架。
vitest官方给出的测试框架对比:cn.vitest.dev/guide/compa…
最终选择vitest的原因:
- 项目是vite,引入vitest可以复用原有的一些配置、插件
- 内置的 TypeScript / JSX 支持,项目中使用了ts,使用vitest就天然支持,而jest需要再引入ts-jest并配置
- vitest主打极速
- 在pnpm test模式下,支持热更新,实时调试
- 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"
}
编写测试脚本
创建测试脚本
- 在对应的组件文件夹下新建
__tests__文件夹 - 在文件夹中新建测试脚本文件
index.test.tsx - 使用
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,输出以下结果:
查看测试报告,跑pnpm report
可在线修改测试脚本和运行,此外还可查看测试覆盖率报告,点击左侧边栏顶部按钮
示例代码
问题记录
- 在
monorepo中的某一个包进行单元测试时,需要把依赖安装在对应包下,不能安装为共享依赖 tooltips组件使用trigger触发mouseenter,通不过测试- 原测试脚本:
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,可以设置定时器延迟执行断言语句。
- 修改后:
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");
});
});