Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试

本文首发于Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试 | 吕立青的博客javascript

欢迎关注知乎专栏 —— 前端的逆袭(凡可 JavaScript,终将 JavaScript。)前端

欢迎关注个人博客知乎GitHub掘金vue


本文的目标

2.1 在Vue应用的单元测试中,对不一样UI组件的单元测试有何不一样?颗粒度该细到什么样的程度?java

// Given
一个有基本的UT知识但没写过Vue测试的新人🚶
// When
当他🚶阅读和练习本文的Vue单元测试的部分
// Then
固然,他可以学会Vue组件在测试当中的几种渲染方式
他可以学会UI组件的分类,特别是交互行为的测试方式
复制代码

组件化与 UI 测试

在组件化出现以前,咱们都压根不谈 UI 的单元测试,哪怕是对于 UI 页面层级的测试来讲都是一件很是困难的事情。其实组件化并不全是为了复用,不少状况下也偏偏是为了分治,从而咱们能够分组件对 UI 页面进行开发,而后分别对其进行单元测试。git

前端组件化已经让 UI 测试变得容易不少,每一个组件均可以被简化为这样一个表达式,即 UI = f(data),这个纯函数返回的只是一个描述 UI 组件应该是什么样子的虚拟 DOM,本质上就是一个树形的数据结构。给这个纯函数输入一些应用程序的状态,就会获得相应的 UI 描述的输出,这个过程不会去直接操做实际的 UI 元素,也不会产生所谓的反作用。github

Vue 组件树的测试

按理来讲按照纯函数这样的思路,Vue 组件的测试应该很简单的说。但与此同时,对 UI 渲染的组件树进行测试依然存在一个问题,从下图中能够看出,越处于上层的组件,其复杂度必然会随之提升。对于最底层的子组件来讲,咱们能够很容易得将其进行渲染并测试其逻辑的正确与否,但对于较上层的父组件来讲,一般来讲就须要对其所包含的全部子组件都进行预先渲染,甚至于最上面的组件须要渲染出整个 UI 页面的真实 DOM 节点才能对其进行测试,这显然是不可取的。数组

Components-Tree

在单元测试中,一般咱们但愿将重点放在做为独立单元进行测试的组件上,并避免间接断言其子组件的行为。此外,对于包含许多子组件的组件,整个 render 树会变得很是之大,而反复 render 全部的子组件可能会减慢单元测试的速度。浏览器

而根据 Mike Cohn 的测试金字塔中所提到的两件事:数据结构

  • 编写不一样粒度的测试
  • 层次越高,你写的测试应该越少

为了维持金字塔形状,一个健康、快速、可维护的测试组合应该是这样的:写许多小而快的单元测试。适当写一些更粗粒度的测试,写不多高层次的端到端测试。注意不要让你的测试变成冰淇淋那样子,这对维护来讲将是一个噩梦,而且跑一遍也须要太多时间。(via 测试金字塔实战 – ThoughtWorks洞见架构

测试金字塔

对于 Vue 组件树来讲,浅渲染(Shallow Rendering)解决了这个问题,也就是说在咱们针对某个上层组件进行测试时,能够不用渲染它的子组件,因此就不用再担忧子组件的表现和行为,这样就能够只对特定组件的逻辑及其渲染输出进行测试了。Vue 官方提供了 @vue/test-utils 可让咱们使用浅渲染这个特性,用于测试虚拟 DOM 对象,即 Vue.component 的实例。

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)
wrapper.vm // the mounted Vue instance
复制代码

Vue 组件的渲染方式

浅渲染 shallowMount(component[, options]) => Wrapper

浅渲染在将一个组件做为一个单元进行测试的时候很是有用,能够确保你的测试不会去间接断言子组件的行为。shallowMount 方法就是 Shallow Rendering 的封装,shallowMountmount 相似返回 mountedrendered Vue 组件的 Wrapper,但只会渲染出组件的第一层 DOM 结构,其嵌套的子组件不会被渲染出来,从而使得渲染的效率更高,单元测试的速度也会更快。

import { shallowMount } from '@vue/test-utils'

describe('Vue Component shallowMount', () => {
  it('should have three <todo /> components', () => {
    const wrapper = shallowMount(App)
    expect(wrapper.find({ name: 'Todo' })).toHaveLength(3)
  })
}
复制代码

全量渲染 mount(component[, options]) => Wrapper

mount 方法则会将 Vue 组件和全部子组件渲染为真实的 DOM 节点,特别是在你依赖真实的 DOM 结构必须存在的状况下,好比说按钮的点击事件。彻底的 DOM 渲染须要在全局范围内提供完整的 DOM API, 这也就意味着 Vue Test Utils 依赖于浏览器环境。

从技术上讲,你能够在真实的浏览器中运行,但因为在不一样平台上启动真实浏览器的复杂性,更建议使用 JSDOM 在虚拟浏览器环境中运行 Node 中的测试。推荐使用 mount 的方法是依赖于一个名为 jsdom的库,它本质上是一个彻底在 JavaScript 中实现的 headless 浏览器。

import { mount } from '@vue/test-utils'

describe('Vue Component Mount', () => {
  it('should delete Todo when click button', () => {
    const wrapper = mount(App)
    const todoLength = wrapper.find('li').length
    wrapper.find('button.delete').at(0).trigger('click')
    expect(wrapper.find('li').length).toEqual(todoLength - 1)
  })
})
复制代码

静态渲染 render(component[, options]) => CheerioWrapper

render 方法则会将 Vue 组件渲染成静态的 HTML 字符串,而返回的则是一个 Cheerio 实例对象,采用的是一个第三方的 HTML 解析库 Cheerio,这是一个类 jQuery 的库,能够在 Node.js 中遍历 DOM。渲染后所返回的 CheerioWrapper 能够用于分析最终结果的 HTML 代码结构,好处是它的 API 跟 shallowMountmount 方法的 API 都基本保持一致。

import { render } from '@vue/test-utils'

describe('Vue Component Render', () => {
  it('should not have .todo-done class', () => {
    const wrapper = render(App)
    expect(wrapper.find('.todo-done').length).toEqual(0)
    expect(wrapper.text()).toContain('<div class="todo"></div>')
  })
})
复制代码

纯字符串渲染 renderToString(component[, options]) => string

renderToString 很简单,顾名思义就是把一个组件渲染成对应的 HTML 字符串,在此再也不赘述。

import { renderedString } from '@vue/test-utils'

describe('Vue Component renderedString', () => {
  it('should have .todo class', () => {
    const renderedString = renderToString(App)
    expect(renderedString).toContain('<div class="todo"></div>')
  })
})
复制代码

实例 Wrapper find() 方法与选择器

从前面的示例代码中能够看到,不管哪一种渲染方式所返回的 wrapper 都有一个 .find() 方法,它接受一个 selector 参数,而后返回一个对应的 wrapper 对象。而 .findAll() 则会返回一个类型相同的 wrapper 对象数组,里面包含了全部符合条件的子组件。在这个对象数组的基础上,at 方法则能够返回指定位置的子组件,trigger 方法用于在组件之上模拟触发某种行为。

@vue/test-utils 中的 Selectors 即选择器,既能够是 CSS 选择器(也支持比较复杂的关系选择器组合),也能够是 Vue 组件 或是一个 option 对象,以便于在 wrapper 对象中能够轻松地指定想要查找的节点。

/* CSS Selector */
wrapper.find('.foo') //class syntax
wrapper.find('input') //tag syntax
wrapper.find('#foo') //id syntax 
wrapper.find('[foo="bar"]') //attribute syntax
wrapper.find('div:first-of-type') //pseudo selectors
复制代码

在下面的示例中,咱们能够经过 Vue 组件构造函数的引用找到该组件,与此同时也能够基于 Vue 组件属性的子集来查找组件和节点,或者经过根据 $ref 选择相应元素。

/* Component Constructor */
import foo from './foo.vue'

const wrapper = shallowMount(app)
expect(wrapper.find(foo).is(foo)).toBe(true)

/* Find Option Object */
const wrapper = appWrapper.find({ name: 'my-button' })
wrapper.trigger('click')

/* Find by refs */
const wrapper = appWrapper.find({ ref: 'myButton' })
wrapper.trigger('click')
复制代码

UI 组件交互行为的测试

咱们不但能够经过 find 方法查找 DOM 元素,还能够经过 trigger 方法在组件上模拟触发某个 DOM 事件,好比 Click,Change 等等。对于浅渲染来讲,事件模拟并不会像真实环境中所预期的那样进行传播,所以咱们必须在一个已经设置好了事件处理方法的实际节点上才可以调用,实际上 .trigger() 方法将会根据模拟的事件触发这个组件的 prop。例如,.trigger('click') 实际上会获取 对应的 clickHandler propsData 并调用它。

it('should trigger event when click button', () => {  
  const clickHandler = jest.fn()
  const wrapper = shallowMount(Foo, {
    propsData: { clickHandler }
  })
  wrapper.trigger('click')
  expect(clickHandler).toHaveBeenCalled()
})
复制代码

关于 nextTick 怎么办?

Vue 会异步的将未生效的 DOM 更新批量应用,以免因数据反复突变而致使的无谓的从新渲染。这也是为何在实践过程当中咱们常常在触发状态改变后用 Vue.nextTick 来等待 Vue 把实际的 DOM 更新作完的缘由。

为了简化用法,Vue Test Utils 同步应用了全部的更新,因此你不须要在测试中使用 Vue.nextTick 来等待 DOM 更新。

注意:当你须要为诸如异步回调或 Promise 解析等操做显性改进为事件循环的时候,nextTick 仍然是必要的。

总结一下

Vue 组件的单元测试是前端 UI 测试组合的基石,单元测试保证了代码库里的每一个组件(被测试的主体)都能按照预期那样工做,它的数量在测试组合中应该远远多于其余类型的测试。其实呢,也不要太拘泥于测试金字塔中各层次的名字,UI 测试显然没必要位于金字塔的最高层,你也彻底能够用 Cypress、Nightwatch 这样的 E2E 框架对 UI 进行单元测试,这个的话咱们就留到后面再聊。

未完待续……

## 单元测试基础

  • [x] ### 单元测试与自动化的意义
  • [x] ### 为何选择 Jest
  • [x] ### Jest 的基本用法
  • [x] ### 该如何测试异步代码?

## Vue 单元测试

  • [x] ### Vue 组件的渲染方式
  • [x] ### Wrapper find() 方法与选择器
  • [x] ### UI 组件交互行为的测试

## Vuex 单元测试

  • [ ] ### CQRS 与 Redux-like 架构
  • [ ] ### 如何对 Vuex 进行单元测试
  • [ ] ### Vue组件和Vuex store的交互

## Vue应用测试策略

  • [ ] ### 单元测试的特色及其位置
  • [ ] ### 单元测试的关注点
  • [ ] ### 应用测试的测试策略

本文首发于Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试 | 吕立青的博客

欢迎关注知乎专栏 —— 前端的逆袭(凡可 JavaScript,终将 JavaScript。)

欢迎关注个人博客知乎GitHub掘金

相关文章
相关标签/搜索