声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
在上文,我们已经知道了如何去实现一个 headless ui
无头组件,我们都知道要想让自己的组件经受得起千锤百炼,那么一个合格的组件测试那是必不可少的;
接下来咱们就开始去实现一个 headless ui
无头组件的测试用例吧!
为了体现咱们的库的多样化,这篇文章的主要基于 Hover Card 无头组件
来实现;
其实 Hover Card 无头组件
功能和上一篇文章所讲的 Popover 无头组件
的功能类似,只能把触发事件由 click 变成了 hover 事件,其它内容几乎大差不大。
一、分析 Hover Card 无头组件功能
1)效果展示
直接上图:
那么我们能看到其实这其中也是包括了这几个关键的子组件:
- HoverCardRoot:根组件
- HoverCardTrigger:触发器
- HoverCardContent:触发内容
- HoverCardArrow:箭头
看到这里是不是和上一篇文章所讲的 Popover 无头组件
很相似,是的,的确很相似。
2)全部组件展示
那么既然我们知道了基本的效果,那么我们看下完整的组件使用案例:
ts
<script setup>
import { HoverCardArrow, HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from '@yi-ui/vue'
</script>
<template>
<HoverCardRoot>
<!-- 触发器 -->
<HoverCardTrigger />
<!-- 触发内容 -->
<HoverCardPortal>
<HoverCardContent>
<div>
render content
</div>
<!-- 触发内容的箭头 -->
<HoverCardArrow />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>
能看到这就是一个完整的的Hover Card 无头组件
使用;
我再介绍一下 HoverCardPortal
是干嘛的:
一个基于 Vue 内置组件 Teleport 封装的子组件,主要作用与 Vue 内置的 Teleport 作用一致
3)代码实现
因为这篇重点不在这里,如果大家硬是要了解如何实现,可以看看我上一篇文章如何实现 Popover 无头组件
,相似度可以达到 80%;
我们这里主要看下代码分布即可:
接下来我们就是主要去实现 __test__
中的代码逻辑.
二、单元测试
1)测试前准备
为了更好的测试到每一个功能点,我们需要安装一些必须的工具库:
- vitest:这个就不解释了
- @vitest/coverage-istanbul:检测代码覆盖率
- @vue/test-utils :官方的底层组件测试库,用来提供给用户访问 Vue 特有的 API。先在测试文件中导入需要测试的 Vue.js 组件,再使用
mount()
方法创建一个组件实例,并可传入 props、数据等参数。 - vitest-axe:测试库,检查程序的可访问性;可访问性测试检查用户界面是否可供每个用户轻松使用,并使我们的应用程序可用于残障人士。
- jsdom:模拟浏览器的 Dom。
- @testing-library/jest-dom :提供了一组自定义 jest 匹配器,可以使用它们来扩展 vitest 的常用 API(例如 toBeInTheDocument),可以使测试用例更具声明性且更易于阅读和维护。
- @testing-library/user-event:尝试模拟触发用户与浏览器交互时在浏览器中发生的真实事件
- @testing-library/vue:与 @vue/test-utils 类似,一个非常轻量级的专注于测试组件而不依赖于实现细节的 Vue 测试库,且包含了 @vue/test-utils 的功能。
我们推荐在应用中使用
@vue/test-utils
测试组件。@testing-library/vue
在测试带有 Suspense 的异步组件时存在问题,在使用时需要谨慎。
以上的工具库就是编写测试用例时所必须的了,下面就开始手把手的开干了~
2)vitest 使用
安装与配置:
这个在之前的文章手动实现一个无头组件库就有介绍了,就不重复赘述了
进入 __test__
目录下新建 index.test.ts
文件:因为一般情况下,执行测试的文件名中必须包含 .test.
或 .spec.
。
常用 API 介绍:
- test:别名又是
it
,定义了一组相关的测试期望, 它接收测试名称和保存测试期望的函数。 - describe:在当前上下文中定义一个新的测试用例,作为一组相关测试、基准以及其他嵌套的测试用例。
- beforeEach:注册一个回调函数,在当前上下文中的每个测试运行前调用。
- afterEach:注册一个回调函数,在当前上下文中的每个测试完成后调用。
- beforeAll:注册一个回调函数,在开始运行当前上下文中的所有测试之前调用一次。
- afterAll:注册一个回调函数,以便在当前上下文中所有测试运行完毕后调用一次。
- expect:主要用于创建断言,也就是判断你写的玩意正不正确。
了解了相关介绍,我们接着往下看;
简单的案例使用:
ts
import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import { ref } from 'vue'
// 因为有三个测试用例,所以 beforeEach 会执行三次
const a = ref(0)
beforeEach(async () => {
a.value = 2
console.log('a 赋值为:', a.value);
})
// 只会执行一次
const b = ref(0)
beforeAll(async () => {
b.value = 3
console.log('b 赋值为:', b.value);
})
describe('一个默认的悬浮组件', () => {
it('测试两个数值相加', () => {
expect.soft(1 + 2).toBe(3) // 期望 1 + 2 等于 3,实际等于 3
})
test('测试两个数值相减', () => {
expect(2 - 1).toBe(1) // 期望 2 - 1 等于 1,实际等于 1
})
it('测试一个数值是否等于 3', () => {
expect(a.value).toBe(3) // 期望 a 等于 3,实际等于 2
})
})
// 因为有三个测试用例,所以 afterEach 会执行三次
afterEach(async () => {
a.value = 0
console.log('初始化 a 的值为:', a.value);
})
// 只会执行一次
afterAll(async () => {
b.value = 0
console.log('初始化 b 的值为:', b.value);
})
执行结果:
到这里我们就已经学会使用了 Vitest 的最最最基本的使用了;
3)Vue3 组件测试(@vue/test-utils)
因为上面我们要测试的是一个成熟的 Vue 组件,那么上面的案例很明显还不达标,所以 @vue/test-utils 库就必不可少了。
而且我们上面也简单的介绍了 @vue/test-utils
库的概念:
Vue 官方的底层组件测试库,用来提供给用户访问 Vue 特有的 API。
3.1 安装
bash
pnpm i @vue/test-utils -Dw
3.2 常用 API 介绍与使用
@vue/test-utils
是 Vue.js 官方提供的用于编写单元测试和集成测试的工具库。下面是一些 @vue/test-utils
中常用的 API 介绍:
-
mount: 一个常用的方法,用于挂载 Vue 组件到一个虚拟 DOM 中,并返回一个包装器。可以通过这个包装器来访问和操作组件。
-
shallowMount: 与
mount
类似,但是它会挂载组件,但不会渲染其子组件。适用于测试一个组件而不涉及其子组件。tsimport { shallowMount, mount } from '@vue/test-utils'; import MyComponent from '@/components/MyComponent.vue'; // 浅挂载 const wrapperShallow = shallowMount(MyComponent); // 完全挂载 const wrapperFull = mount(MyComponent);
-
find: 用于查找组件中的子元素或子组件。
tsconst wrapper = mount(MyComponent); const button = wrapper.find('button');
-
findAll: 与
find
类似,但会返回所有匹配的元素或组件。ts// 查找所有段落 const wrapper = mount(MyComponent); const all_p = wrapper.findAll('p');
-
setData: 用于设置组件的 Data 数据。
tsconst wrapper = mount(MyComponent); wrapper.setData({ count: 10 });
-
setProps: 用于设置组件的 props。
tsconst wrapper = mount(MyComponent); wrapper.setProps({ message: 'Hello' });
-
attributes 和 classes
attributes
用于获取元素的属性,classes
用于获取元素的类名。tsconst wrapper = mount(MyComponent); // 获取按钮的属性 const buttonAttributes = wrapper.find('button').attributes(); // 获取段落的类名 const paragraphClasses = wrapper.find('p').classes();
-
emitted 和 emittedByOrder
emitted
用于检查事件是否被触发,emittedByOrder
用于获取按顺序触发的事件。tsconst wrapper = mount(MyComponent); // 检查事件是否触发 expect(wrapper.emitted().submit).toBeTruthy(); // 获取按顺序触发的事件 const events = wrapper.emittedByOrder();
-
trigger: 用于触发组件的事件。
tsconst wrapper = mount(MyComponent); wrapper.find('button').trigger('click');
-
vm: 可以访问 Vue 组件实例。
tsconst wrapper = mount(MyComponent); const vm = wrapper.vm;
-
destroy:用于卸载组件。
tsconst wrapper = mount(MyComponent); wrapper.destroy();
这些是 @vue/test-utils
中一些常用的 API,用于编写 Vue.js 组件的单元测试和集成测试。
当然还有更加高级的功能,如 contains
, isVueInstance
等。
通过这些 API,我们可以很方便的编写测试用例,模拟用户操作和验证组件O不OK。
4)模拟用户行为测试(@testing-library/user-event)
我们都知道,headless ui
组件库主要是一个交互逻辑行为,所以在测试这块,我们必须要对此下重手;
其实上面的 @vue/test-utils
库能通过 trigger
触发一些基本的用户事件,例如点击鼠标移入移出等等;
但是 @vue/test-utils
是专为Vue.js设计,深度集成 Vue 特性,提供的是 Vue 组件测试一站式解决方案。
所以我们为了遵循以用户为中心
的测试原则,且提供的 API 能很好模拟用户视角下的操作,鼓励测试代码与实际用户交互保持一致。
我们还需要引入@testing-library/user-event
来更逼真地模拟用户交互。
4.1安装&介绍
bash
pnpm i @testing-library/user-event -Dw
@testing-library/user-event
是一个 JavaScript 库,专为模拟用户与网页界面之间的真实交互行为而设计。
它与 @testing-library/dom
和特定框架的 Testing Library(如@testing-library/vue
)紧密结合,提供了高级的、符合浏览器行为的事件触发机制。
使用@testing-library/user-event
的主要目的是确保测试代码能够精确地模拟用户在浏览器中的操作,如鼠标点击、键盘输入、拖放等,从而更有效地测试组件的交互逻辑和响应。
4.2 常见方法示例
-
文本输入:
userEvent.type(element: Element, text: string, options?: TypeOptions)
模拟用户在指定元素上逐字符输入文本。可以传递选项(如delay
)来控制输入速度。
-
鼠标点击与悬停:
userEvent.click(element: Element, options?: ClickOptions)
模拟用户在指定元素上单击。可以传递选项(如button
、ctrlKey
等)来模拟不同的鼠标按键和修饰键状态。userEvent.hover(element: Element)
模拟用户将鼠标光标悬停在指定元素上。
-
键盘交互:
userEvent.keyboard(text: string, init?: KeyboardEventInit)
模拟用户按下一系列键盘键。可以传递KeyboardEventInit
对象来设置修饰键和其他属性。
-
选择与复选:
userEvent.selectOptions(select: Element, values: Array<string | Element>)
模拟用户在下拉选择框(<select>
元素)中选择指定的选项。userEvent.check(element: Element, init?: MouseEventInit)
模拟用户勾选一个复选框(<input type="checkbox">
元素)。userEvent.uncheck(element: Element, init?: MouseEventInit)
模拟用户取消勾选一个复选框。
-
拖放操作:
userEvent.dragAndDrop(source: Element, target: Element, init?: DragEventInit)
模拟用户将一个元素拖放到另一个元素上。
此外,@testing-library/user-event
还提供了其他方法来模拟用户交互,如dblClick
(双击)、clear
(清除输入框内容)、tab
(按Tab键切换焦点)等。
这些方法力求尽可能地模拟真实用户的交互过程,包括触发相关的事件序列和浏览器默认行为,使得测试更加贴近实际使用场景。
4.3 使用
新建 HoverCard.vue:
ts
<template>
<div class="wrapper">测试 axe</div>
<button @click="onHandler">按钮点击</button>
<div class="hover-card" v-if="visible">
显示的内容
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const onHandler = () => {
console.log('点击了按钮')
visible.value = !visible.value
}
</script>
编辑 index.test.ts:
ts
import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
// import { axe } from 'vitest-axe'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import userEvent from '@testing-library/user-event'
describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
wrapper = mount(HoverCard, { attachTo: document.body })
})
it('测试 HoverCard 组件中的 click 事件', async () => {
const user = userEvent.setup()
const button = wrapper.find('button')
await user.click(button.element)
expect(wrapper.find('.hover-card').isVisible()).toBe(true)
})
})
运行结果:
5)无障碍可访问性测试(vitest-axe)
根据之前的文章介绍,我们了解到了,Headless UI 还有一个很重要的概念,那就是可访问性,所谓可访问性那就是要符合
5.1 介绍
vitest-axe 是一个用于 Vue.js 应用程序的可访问性测试工具,它基于 Axe Core 来研发的,同时是 fork 了 jest-axe 来进行二次开发,可以帮助开发人员检测应用程序中的无障碍问题。
介绍:Axe-Core 是一个流行的开源库,用于自动化检测网页的可访问性问题,确保网站符合 WAI-ARIA 和 WCAG(Web Content Accessibility Guidelines)等无障碍标准,提升残障用户使用体验。
5.2 常用 API 介绍
-
axe:参数为待测元素(通常是DOM节点),执行 Axe Core 的检查,并返回包含检查结果的对象;然后使用Vitest 的 expect 断言 api 来验证检查结果中存在的可访问性违规。
-
configureAxe:这个函数用于配置 Axe Core,可以设置一些选项,例如忽略某些规则等。
5.3 使用
第一步:安装
bash
pnpm i vitest-axe -Dw
第二步:配置 vitest.setup.ts 启用无障碍测试支持
ts
import { expect } from 'vitest'
import type { AxeMatchers } from 'vitest-axe/matchers'
import * as matchers from 'vitest-axe/matchers'
import { configureAxe } from 'vitest-axe'
import "vitest-axe/extend-expect";
expect.extend(matchers)
// 拓展 .d.ts 的属性,例如 matchers 中的 toHaveNoViolations,如果你的项目没有用到 ts,可以忽略
declare module 'vitest' {
export interface Assertion extends AxeMatchers {}
export interface AsymmetricMatchersContaining extends AxeMatchers {}
}
然后在 vitest.config.ts 中添加 setupFiles: './vitest.setup.ts',
即可。
然后在 tsconfig.json 中添加 include: ['./vitest.setup.ts'],
即可。
第三步:测试文件 index.test.ts 中正式编写
首先创建一个组件 HoverCard.vue:
ts
<template>
<div class="wrapper">测试 axe</div>
</template>
<script setup lang="ts">
</script>
然后再 index.test.ts 中引入编写:
ts
import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import { axe } from 'vitest-axe'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
// 挂载一个 vue 组件
wrapper = mount(HoverCard, { attachTo: document.body })
})
it('测试 HoverCard 组件是否渲染成功', () => {
expect(wrapper.exists()).toBe(true)
})
test('测试 HoverCard 组件是否通过了可访问性测试', async () => {
expect(await axe(wrapper.element)).toHaveNoViolations()
})
})
第四步:渲染结果
咱们会看到可访问性测试没通过,这是咋回事呀,那当然是因为我们写的 HoverCard.vue 不符合要求啦,所以接下来我们再重新改造一下。
三、编写测试组件代码
1)前提
在上面,我们介绍了 HoverCard 无头组件的基本使用,代码如下:
ts
<script setup>
import { HoverCardArrow, HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from '@yi-ui/vue'
</script>
<template>
<HoverCardRoot>
<!-- 触发器 -->
<HoverCardTrigger />
<!-- 触发内容 -->
<HoverCardPortal>
<HoverCardContent>
<div>
render content
</div>
<!-- 触发内容的箭头 -->
<HoverCardArrow />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>
所以接下来,我们就根据这个来进行自定义拓展了
我们来实现一个如下的组件功能:
2)代码实现
ts
<script setup lang="ts">
import { ref } from 'vue';
import {
HoverCardArrow,
HoverCardContent,
HoverCardPortal,
HoverCardRoot,
HoverCardTrigger,
} from '@/HoverCard';
const hoverState = ref(false);
</script>
<template>
<HoverCardRoot v-model:open="hoverState">
<HoverCardTrigger
class="inline-block cursor-pointer rounded-full shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] outline-none focus:shadow-[0_0_0_2px_white]"
>
悬浮鼠标
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent
class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] rounded-md bg-white p-5 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] data-[state=open]:transition-all"
:side-offset="5"
>
<div class="flex flex-col gap-[7px]">
<div class="flex flex-col gap-[15px]">悬浮内容1</div>
<div class="flex flex-col gap-[15px]">悬浮内容2</div>
<div class="flex flex-col gap-[15px]">悬浮内容3</div>
<div class="flex flex-col gap-[15px]">悬浮内容4</div>
</div>
<HoverCardArrow class="fill-white" :width="8" />
</HoverCardContent>
</HoverCardPortal>
</HoverCardRoot>
</template>
很明显我这里是使用了 tailwindcss 风格来写的样式,如果你写的话,可以不用写样式,直接使用即可;
那么接下来我们就要对这个文件来进行单元测试用例的编写了。
3)单元测试用例关联
3.1 测试组件是否渲染成功
ts
import { beforeEach, afterEach, beforeAll, afterAll, describe, expect, test, it } from 'vitest'
import HoverCard from './HoverCard.vue'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
describe('一个默认的悬浮组件', () => {
let wrapper: VueWrapper<InstanceType<typeof HoverCard>>
beforeEach(() => {
wrapper = mount(HoverCard, { attachTo: document.body })
})
it('测试 HoverCard 组件是否渲染成功', () => {
expect(wrapper.exists()).toBe(true)
})
})
3.2 测试组件的可访问性测试
ts
import { axe } from 'vitest-axe'
it('测试 HoverCard 组件是否通过了可访问性测试', async () => {
expect(await axe(wrapper.element)).toHaveNoViolations()
})
3.3 测试组件的用户模拟事件
ts
describe('模拟 HoverCard 组件中的鼠标移入100ms后', () => {
it('是否应该通过可访问性测试', async () => {
await wrapper.find('a').trigger('mouseenter')
await sleep(100)
expect(await axe(document.body)).toHaveNoViolations()
})
})
4)运行结果
那么到这里,我们的组件单元测试就写完了,其实细看整体还是很简单的,只要弄懂几个关键库的基本使用基本就可以解决 80% 的问题了。
四、测试覆盖率
运行
bash
pnpm coverage
运行结束后会生成一个 coverage 文件,在这里就可以看到你的测试案例的覆盖率了
打开对应的 index.html
当然覆盖率可视化不仅只有这个,比如还有 vitest/ui
、storybook
等等,这些取舍就看你个人的了,大家明白即可。
总结
本文主要介绍了,headless ui
的 基本组件测试
,还有从用户角度出发的 E2E测试
,我相信大家都能有所收获,肯定会对 vue 组件的测试有一定的很深入的了解。
接下来的安排:
- 文档撰写
- 支持 Nuxt 调试
- 打包构建
Headless UI 往期相关文章:
- 在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?
- 实战开始 🚀 在 React 和 Vue3 中使用 Headless UI 无头组件库
- 无头组件库既然这么火 🔥 那么我们自己手动实现一个来完成所谓的 KPI 吧
- 泰裤辣 🚀 原来实现一个 Popover 无头组件比传统组件简单辣么多!!
如果想跟我一起讨论技术吹水摸鱼 , 欢迎加入前端学习群聊
如果扫码人数满了,可以扫码添加个人 vx 拉你:JeddyGong
感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏
哦 ~
关注我,带您一起搞前端 ~