Vitest
Vitest 是一个测试框架,类似老框架 Jest,用于运行测试。Vitest 最大的优点是可以和 Vite 整合起来,减少配置复杂度(反过来说,如果你不用 Vite 的话 Vitest 不一定是最好的选择)。
如果你在 Vite 的基础上安装 Vitest,只需要安装 ,然后在 vite.config.ts
中配置 test
字段即可,相关配置项可以参考官方文档。
作为一个测试框架,那么最关键的元素也就还是 describe
、test
(it
)和 expect
:
比较麻烦的是 mocking。
mock 就是在测试中制造一些测试数据,对于一些不需要测试的函数,可以用 mock 的方式隐藏细节,直接提供需要的数据。在调用复杂的 useX
或 ajax 请求时,特别需要 mock。
下面是一些常见的 MockInstance Method
vi.spyOn
spyOn
可以用于监听一个函数,计算调用次数,也可以使用 mockReturnValue
等方法伪造函数。
这是一个官方例子:
可见 spyOn
可以监视整个模块,干预模块内某个输出值的行为,包括变量和函数。但是实际上却有一些问题,自己定义的模块是可以的,某些模块却不能正确 spyOn
,例如 vue-router:
测试运行失败,返回 TypeError: Cannot redefine property: useRoute
,原因未明(后面会提供解决方案)。
vi.fn
vi.fn() 和 vi.spyOn() 共享相同的方法,但是只有 vi.fn() 的返回结果是可调用的。
根据官网的描述,vi.fn()
返回可执行的函数,而 vi.spyOn()
只对目标行为进行修改。在实践中暂时还是觉得 vi.spyOn()
比较实用,一般只会修改目标行为,让页面模拟调用,并不需要在测试文件上调用模拟函数。
vi.mock
使用提供的 path 替换所有导入的模块为另一个模块。对 vi.mock 的调用是提升的,因此您在哪里调用它并不重要。它将始终在所有导入之前执行。
因为 vi.mock
会被提升,所以不需要在 beforeEach
里写 vi.mock,写了也只有一个生效
因此上面路由的问题可以如此解决:
其实测试文件真的有一点反直觉,很难从代码一下反应出 useRoute
用的到底是什么。虽然你看到用 import { useRoute } from 'vue-router'
,但因为使用了 vi.mock
,最终拿到的 useRoute
其实是 vi.fn()
。
P.S. vi.mocked
其实是一个 Typescript 辅助函数,可以帮助你通过类型检测,如果是使用 JavaScript 的话可以忽略。
在一些有深层依赖的组件来说,很多基础依赖都会影响测试运行(是的,跟正常运行一样,不只是那个文件的直接依赖,无论间接直接,都会被调用),一一在每个测试文件写 mock 函数是不现实的,针对这个问题,我们可以使用 __mocks__
文件夹。
__mocks__
文件夹
mock
方法的函数签名是 (path: string, factory?: () => unknown) => void
。若不传入 factory
,Vitest 会在 __mocks__
文件夹找同名文件,将其当成要引入的对象,具体目录结构如下:
使用例子:
P.S. 如果引入的路径带有 @
等别名,那么 mock 的时候也需要使用别名,如 vi.mock('@/api/test.ts')
环境变量与全局变量
相比函数的测试,用户界面组件测试才是前端的大问题,测试框架 Vitest 本身并不能解决这个问题,我们还需要使用 Vue Test Utils。
钩子
什么时候使用 beforeEach
?
使用 vi.clearAllMocks
等方法清除 mock,每个测试用例需要使用相同 mock 时可以在这里统一处理。
断言相关
UI 界面
Vitest UI 为你提供 UI 界面,可以快捷地重新运行特定测试,并且可以看到测试耗时和 log,对测试本身的优化十分实用。
Vue Test Utils
Vue Test Utils 简称 VTU,用于模拟挂载组件和触发事件。
模拟组件渲染
上面的代码就是在测试中用 mount
函数模拟挂载 TodoApp
组件,并且可以在第二个参数 options
中传入 data
、props
、plugins
等配置,在得到的结果 wrapper
中对元素模拟操作,实现测试的效果。
组件操作
返回的 wrapper
有这些方法,可以使用 setData
等方法修改组件值,也可以通过 vm
属性访问 ComponentPublicInstance
,这些方法和属性提供了测试组件内部运行细节的方法,不过这只是测试的一个流派,后面说到的 Vue Testing Library 就推荐我们尽量贴近实际操作。
操作之后判断结果是否正确的方法是检查某些元素是否存在、数量是否正确,类似:expect(wrapper.find('p').exists()).toBe(false)
和 expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
。
Vue Testing Library
Vue Testing Library 是基于 VTU 的一层封装。
隐藏细节的测试
Testing Library encourages you to avoid testing implementation details like the internals of a component you’re testing (though it’s still possible). The Guiding Principles of this library emphasize a focus on tests that closely resemble how your web pages are interacted by the users.
Testing Library 不主张测试组件的实现细节,它会刻意隐藏 instance
等信息,让你难以测试实例上的方法,你需要通过页面交互后,检查页面元素变化是否正确,以此判断测试是否通过。所以主要函数 render(Component, options)
的 options
也是基于 Vue Test Utils 本身的 options
,基本上就是作为一个中介把参数传过去,然后返回查询对象。
使用 VTL 与否还得是看你写测试的习惯。不过 VTL 的存在当然证明忽略细节的测试有他的合理之处,这篇文章 Testing Implementation Details 向你解析了为何不要测试实现细节,大家可以了解一下。
因为隐藏了细节,所以 VTL 的测试方式就相对更简单了,举一个经典例子:
render
本质就是对 VTU mount
的包装,而 queryAllByText
之类的方法其实就是很粗暴的遍历匹配:
https://github.dev/testing-library/dom-testing-library
所以 VTL 会比直接用 VTU 慢很多。而且因为无法进行细节操作,一些函数例如 emit、子组件的方法会无法调用,函数测试率略微下降。
另外如果你使用了一些 UI 库,下拉列表选择之类的模拟会比较麻烦,如果直接用 VTU 的话可以用 setValue
,但 VTL 必须模拟点击打开下拉再模拟点击选择。
cheatsheet
这一页 cheatsheet 基本告诉了你所有测试方法。
元素查询
简单来说是 3 个动词:
- get:查找失败抛错
- find:自带 waitFor,查找失败抛错
- query:查找失败返回 null
以上 3 个动词加 All 查找全部,注意,如果不加 All 但是查询到多个元素会抛错。
8 个目标:
- LabelText
- PlaceholderText
- Text
- DisplayValue
- AltText
- Title
- Role
- TestId
如果使用了 UI 库,触发组件可能会有一定困难,可以尝试使用 container.querySelector
寻找元素进行精准操作。
事件分发
在 VTL 中可以通过下面的方法模拟用户操作:
本质上是借助浏览器事件相关 API:
事件类型可参考 MDN | Event
实践
新建一个 Vite 项目,然后直接安装 Vitest 和 VTU:
并添加两个命令:
在 tsconfig.json
加点全局类型:
在 vite 配置中加入 vitest 配置,确实,vitest 可以直接读 vite 配置,但是如果使用 ts 的话类型校验会报错,结果还是新建文件比较好:
现在直接运行 npm run test
便能进行最简单的测试,试着运行吧:
虽然没有效果,但是可以从中知道测试文件的范围。在我现在的项目中所有测试全放在 src
下的 __test__
文件夹,测试文件夹的结构与 src
保持一致,例如:
奇葩需求怎么测试
例如拖拽组件,在测试时可以忽略拖拽操作,把拖拽 mock 为其他操作,然后测试运行结果。当然这也是没办法中的办法而已。
选择 checkbox/radio
至于为什么在找 role 的时候传入 name 可以找到,我还没研究出确切原因,但是下面这两个链接可以提供一定参考:
总结
- Vitest 负责测试运行
- 测试核心关键字 1:mock
- 测试核心关键字 2:断言
- Vue Test Utils 负责模拟组件挂载,提供组件测试的可能性
- VTU 的测试方式是用选择器选择元素修改数据,甚至获取
vm
直接操作,然后检查数据是否正确 - Vue Testing Library 是 VTU 的二次封装,主张接近用户操作的测试
- VTL 的测试方式是用可以看到文字选择元素,对元素操作,然后检查新元素是否存在
最后的最后还是要说一句,即使忽略细节测试,也很容易测漏,漏了再补,久而久之也是一大堆测试,而且某些需求改动也可能造成测试大规模修改,如果不是工期非常充足,请不要写测试。
杂项与排雷
- 遇到奇怪的报错很可能是有模块没 mock,导致运行错误,进而引起神奇的错误
- VTU 的挂载参数可以解决全局插件、组件等问题
ByRole
查询性能低,遇到测试太慢的情况可以用ByTestId
(叠加 waitFor 更慢)- 测试运行时间相关的数据含义
- 前端测试慢?其实不只是你,大家都这么慢
- 是否需要 reset?
其他测试工具
E2E 测试工具
- Cypress,测试编写方法有额外的学习成本,(怨念:而且他连 tab 都无法按)
- Playwright
- Cypress vs Playwright