Vue 组件的单元测试

基本的示例

单元测试是软件开发非常基础的一部分。单元测试会封闭执行最小化单元的代码,使得添加新功能和追踪问题更容易。Vue 的单文件组件使得为组件撰写隔离的单元测试这件事更加直接。它会让你更有信心地开发新特性而不破坏现有的实现,并帮助其他开发者理解你的组件的作用。

这是一个判断一些文本是否被渲染的简单的示例:

  1. <template>
  2. <div>
  3. <input v-model="username">
  4. <div
  5. v-if="error"
  6. class="error"
  7. >
  8. {{ error }}
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'Hello',
  15. data () {
  16. return {
  17. username: ''
  18. }
  19. },
  20. computed: {
  21. error () {
  22. return this.username.trim().length < 7
  23. ? 'Please enter a longer username'
  24. : ''
  25. }
  26. }
  27. }
  28. </script>
  1. import { shallowMount } from '@vue/test-utils'
  2. import Hello from './Hello.vue'
  3. test('Hello', () => {
  4. // 渲染这个组件
  5. const wrapper = shallowMount(Hello)
  6. // `username` 在除去头尾空格之后不应该少于 7 个字符
  7. wrapper.setData({ username: ' '.repeat(7) })
  8. // 确认错误信息被渲染了
  9. expect(wrapper.find('.error').exists()).toBe(true)
  10. // 将名字更新至足够长
  11. wrapper.setData({ username: 'Lachlan' })
  12. // 断言错误信息不再显示了
  13. expect(wrapper.find('.error').exists()).toBe(false)
  14. })

上述代码片段展示了如何基于 username 的长度测试一个错误信息是否被渲染。它展示了 Vue 组件单元测试的一般思路:渲染这个组件,然后断言这些标签是否匹配组件的状态。

为什么要测试?

组件的单元测试有很多好处:

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构自动化测试使得大团队中的开发者可以维护复杂的基础代码。

起步

Vue Test Utils 是 Vue 组件单元测试的官方库。Vue CLIwebpack 模板对 Karma 和 Jest 这两个测试运行器都支持,并且在 Vue Test Utils 的文档中有一些引导

实际的例子

单元测试应该:

  • 可以快速运行
  • 易于理解
  • 只测试一个独立单元的工作我们在上一个示例的基础上继续构建,同时引入一个工厂函数 (factory function)使得我们的测试更简洁更易读。这个组件应该:

  • 展示一个“Welcome to the Vue.js cookbook”的问候语

  • 提示用户输入用户名
  • 如果输入的用户名少于七个字符则展示错误信息让我们先看一下组件代码:
  1. <template>
  2. <div>
  3. <div class="message">
  4. {{ message }}
  5. </div>
  6. Enter your username: <input v-model="username">
  7. <div
  8. v-if="error"
  9. class="error"
  10. >
  11. Please enter a username with at least seven letters.
  12. </div>
  13. </div>
  14. </template>
  15. <script>
  16. export default {
  17. name: 'Foo',
  18. data () {
  19. return {
  20. message: 'Welcome to the Vue.js cookbook',
  21. username: ''
  22. }
  23. },
  24. computed: {
  25. error () {
  26. return this.username.trim().length < 7
  27. }
  28. }
  29. }
  30. </script>

我们应该测试的内容有:

  • message 是否被渲染
  • 如果 errortrue,则 <div class="error"> 应该展示
  • 如果 errorfalse,则 <div class="error"> 不应该展示我们的第一次测试尝试:
  1. import { shallowMount } from '@vue/test-utils'
  2. import Foo from './Foo.vue'
  3. describe('Foo', () => {
  4. it('renders a message and responds correctly to user input', () => {
  5. const wrapper = shallowMount(Foo, {
  6. data: {
  7. message: 'Hello World',
  8. username: ''
  9. }
  10. })
  11. // 确认是否渲染了 `message`
  12. expect(wrapper.find('.message').text()).toEqual('Hello World')
  13. // 断言渲染了错误信息
  14. expect(wrapper.find('.error').exists()).toBeTruthy()
  15. // 更新 `username` 并断言错误信息不再被渲染
  16. wrapper.setData({ username: 'Lachlan' })
  17. expect(wrapper.find('.error').exists()).toBeFalsy()
  18. })
  19. })

上述代码有一些问题:

  • 单个测试断言了不同的事情
  • 很难阐述组件可以处于哪些不同的状态,以及它该被渲染成什么样子接下来的示例从这几个方面改善了测试:

  • 每个 it 块只做一个断言

  • 让测试描述更简短清晰
  • 只提供测试需要的最小化数据
  • 把重复的逻辑重构到了一个工厂函数中 (创建 wrapper 和设置 username 变量)更新后的测试
  1. import { shallowMount } from '@vue/test-utils'
  2. import Foo from './Foo'
  3. const factory = (values = {}) => {
  4. return shallowMount(Foo, {
  5. data () {
  6. return {
  7. ...values
  8. }
  9. }
  10. })
  11. }
  12. describe('Foo', () => {
  13. it('renders a welcome message', () => {
  14. const wrapper = factory()
  15. expect(wrapper.find('.message').text()).toEqual("Welcome to the Vue.js cookbook")
  16. })
  17. it('renders an error when username is less than 7 characters', () => {
  18. const wrapper = factory({ username: '' })
  19. expect(wrapper.find('.error').exists()).toBeTruthy()
  20. })
  21. it('renders an error when username is whitespace', () => {
  22. const wrapper = factory({ username: ' '.repeat(7) })
  23. expect(wrapper.find('.error').exists()).toBeTruthy()
  24. })
  25. it('does not render an error when username is 7 characters or more', () => {
  26. const wrapper = factory({ username: 'Lachlan' })
  27. expect(wrapper.find('.error').exists()).toBeFalsy()
  28. })
  29. })

注意事项:

在一开始,工厂函数将 values 对象合并到了 data 并返回了一个新的 wrapper 实例。这样,我们就不需要在每个测试中重复 const wrapper = shallowMount(Foo)。另一个好处是当你想为更复杂的组件在每个测试中伪造或存根一个方法或计算属性时,你只需要声明一次即可。

额外的上下文

上述的测试是非常简单的,但是在实际情况下 Vue 组件常常具有其它你想要测试的行为,诸如:

  • 调用 API
  • Vuex 的 store,commit 或 dispatch 一些 mutation 或 action
  • 测试用户交互我们在 Vue Test Utils 的教程中提供了更完整的示例展示这些测试。

Vue Test Utils 及庞大的 JavaScript 生态系统提供了大量的工具促进 100% 的测试覆盖率。单元测试只是整个测试金字塔中的一部分。其它类型的测试还包括 e2e (端到端) 测试、快照比对测试等。单元测试是最小巧也是最简单的测试——它们通过隔离单个组件的每一个部分,来在最小工作单元上进行断言。

快照比对测试会保存你的 Vue 组件的标记,然后比较每次新生成的测试运行结果。如果有些东西改变了,开发者就会得到通知,并决定这个改变是刻意为之 (组件更新时) 还是意外发生的 (组件行为不正确)。

端到端测试致力于确保组件的一系列交互是正确的。它们是更高级别的测试,例如可能会测试用户是否注册、登录以及更新他们的用户名。这种测试运行起来会比单元测试和快照比对测试慢一些。

单元测试中开发的时候是最有用的,即能帮助开发者思考如何设计一个组件或重构一个现有组件。通常每次代码发生变化的时候它们都会被运行。

高级别的测试,诸如端到端测试,运行起来会更慢很多。这些测试通常只在部署前运行,来确保系统的每个部分都能够正常的协同工作。

更多测试 Vue 组件的知识可翻阅核心团员 Edd Yerburgh 的书《测试 Vue.js 应用》

何时避免这个模式

单元测试是任何正经的应用的重要部分。一开始,当你对一个应用的愿景还不够清晰的时候,单元测试可能会拖慢开发进度,但是一旦你的愿景建立起来并且有真实的用户对这个应用产生兴趣,那么单元测试 (以及其它类型的自动化测试) 就是绝对有必要的了,它们会确保基础代码的可维护性和可扩展性。