最近 Vue Test Utils 2.0 正式版终于发布了!Vue Test Utils(VTU)帮助你为 Vue 组件编写测试。然而,VTU 的功能也有限,比如我们无法使用包裹器方法 setData
来修改 script setup 中的响应式数据。以下是一些建议,帮助你编写易于测试的代码,并编写有意义且易于维护的测试。
以下列表提供了一般指导,对于常见情况可能会有所帮助。
不要测试实现细节
从用户的角度来看,以输入和输出为思考方式。大致上,这是你在为 Vue 组件编写测试时应考虑的所有内容:
输入 | 示例 |
---|---|
交互 | 点击、输入等任何“人类”交互 |
Props | 组件接收的参数 |
数据流 | 来自 API 调用、数据订阅等的数据流 |
输出 | 示例 |
---|---|
DOM 元素 | 渲染到文档中的任何可观察节点 |
事件 | 通过 $emit 触发的事件 |
副作用 | 例如 console.log 或 API 调用 |
其他所有内容都是实现细节。
请注意,此列表不包括诸如内部方法、中间状态甚至数据等元素。
经验法则是测试不应在重构时失败,也就是说,当我们在不改变行为的情况下更改其内部实现时,测试不应该失败。如果发生这种情况,则测试可能依赖于实现细节。
例如,假设有一个基本的计数器组件,其中包含一个按钮来递增计数器:
<script lang="ts">
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}
</script>
<template>
<p class="paragraph">Times clicked: {{ count }}</p>
<button @click="increment">increment</button>
</template>
我们可以编写以下测试:
// Counter.test.ts
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('counter text updates', async () => {
const wrapper = mount(Counter)
const paragraph = wrapper.find('.paragraph')
expect(paragraph.text()).toBe('Times clicked: 0')
await wrapper.setData({ count: 2 })
expect(paragraph.text()).toBe('Times clicked: 2')
})
请注意,这里我们更新了其内部数据,并且我们还依赖于细节(从用户的角度来看),例如 CSS 类名。
请注意,更改数据或 CSS 类名都会导致测试失败。但组件仍将按预期工作。这被称为误报(false positive)。
相反,下面的测试试图坚持使用上面列出的输入和输出:
// Counter.test.ts
test('text updates on clicking by VTU', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Times clicked: 0')
const button = wrapper.find('button')
await button.trigger('click')
await button.trigger('click')
expect(wrapper.text()).toContain('Times clicked: 2')
})
诸如 Vue Testing Library(VTL)之类的库是基于这些原则构建的。VTL 遵从测试应该更接近用户与应用程序的实际交互,而不仅仅关注内部实现细节。它鼓励开发人员编写与用户行为相对应的测试代码,从而增加测试的可读性和可维护性。
// Counter.test.ts
import { fireEvent, render } from '@testing-library/vue'
import '@testing-library/jest-dom'
test('text updates on clicking by VTL', async () => {
const { getByText, getByRole } = render(Counter)
const text = getByText('Times clicked: 0')
expect(text).toBeInTheDocument()
const button = getByRole('button', { name: /increment/i })
await fireEvent.click(button)
await fireEvent.click(button)
expect(text).toHaveTextContent('Times clicked: 2')
})
和 VUT 编写的组件测试二相比,VTL 编写的组件测试更语义化,而且更通用。VTL 为包括 React、Angular 和 Vue 等框架封装 API,更适合测试多框架的 UI 组件库。因此强烈推荐尽量使用 VTL 组件测试,不得不处理组件实例时才使用 VUT 。
你的测试越像你的软件的使用方式,你就会更有信心。
尽量不要使用 setData
VTU 2.x 主要目标是测试 Vue 3 组件,目前该工具库对选项式 API 支持要比组合式 API 好,比如包裹器方法 setData
或挂载选项 data
不会修改组合式 API 的 setup() 数据。
将之前的 Counter.vue
替换成下面的 script setup 语法:
<script lang="ts" setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<template>
<p class="paragraph">Times clicked: {{ count }}</p>
<button @click="increment">increment</button>
</template>
如你所见,测试一会失败。后面两个测试仍会成功,可见编写测试的组件是遵从最佳实践是很重要的。
FAIL src/vtu/easy/TheCounter.test.ts > counter text updates
TypeError: Cannot add property count, object is not extensible
❯ node_modules/@vue/test-utils/dist/vue-test-utils.esm-bundler.mjs:219:25
注意:目前,我们可以通过包裹器当前组件的 setupState
属性来修改 script setup 中的响应式数据:
test('counter text updates', async () => {
const wrapper = mount(Counter)
const paragraph = wrapper.find('.paragraph')
expect(paragraph.text()).toBe('Times clicked: 0')
wrapper.getCurrentComponent().setupState.count = 2
await wrapper.vm.$nextTick()
expect(paragraph.text()).toBe('Times clicked: 2')
})
这个属性藏得很深😁,以后有可能被官方取消,而且也不是最佳实践,慎用。
如果刚好将响应式数据绑定到input
,是可以试一试setValue()
方法
构建更小、更简单的组件
一个经验法则是,如果组件功能较少,则更容易进行测试。
将组件分解为较小的组件将使它们更易于组合和理解。以下是一些建议,可以使组件更简单。
提取 API 调用
通常,你将在整个应用程序中执行多个 HTTP 请求。从测试的角度来看,HTTP 请求为组件提供了输入,组件也可以发送 HTTP 请求。
如果对测试 API 调用不熟悉,请查看进行 HTTP 请求指南。
提取复杂的方法
有时,组件可能包含复杂的方法、执行繁重的计算或使用多个依赖项。
这里的建议是将此方法提取出来并导入到组件中。这样,你可以使用 Jest 或任何其他测试运行器单独测试该方法。
这样做的另一个好处是,组件更容易理解,因为复杂逻辑被封装在另一个文件中。
此外,如果复杂的方法难以设置或速度较慢,你可能希望模拟它以使测试更简单和更快。进行 HTTP 请求的示例是一个很好的例子-axios 是一个相当复杂的库!
在编写组件之前编写测试
如果在编写代码之前编写测试,就不可能编写无法测试的代码!
入门指南提供了一个示例,说明在编写代码之前编写测试如何导致可测试的组件。它还可以帮助你检测和测试边缘情况。
评论 (0)