文章目录

  • 前言
  • 介绍
  • 语法实战
  • Shallow Rendering
  • DOM结构
  • 测试Props
  • 总结

前言

  • 公共组件
    在我们编写测试用例之前,首先需要确认我们的目标组件到底是什么?我的理解为,在我们的项目中通常会分为两种类型的组件,一种为业务组件,另一种为抽象出来的公共组件。通常,我们在编写测试用例时从公共组件优先考虑,这部分组件变动不大且涉及范围更广泛,优先保证这部分组件的正常运行对于项目而言是基石般的重要,且因为公共组件通常不会因为业务或是界面的改动而产生大范围的重构,所以测试用例通常也不会需要重写或是废弃的情况。目前大部分的团队编写测试用例是只考虑面向公共组件的,这样可以把成本压缩到最低并且可以保证不错的收益。
  • 业务组件
    根据上文描述,那对于业务变动相对频繁,重构几率颇高的业务组件而言是否就不需要编写测试用例了呢?这么做会不会得不偿失?我的想法是如果资源紧张,并且在项目处于一个初期发展且不太稳定的时期时,确实不适合着手对于部分业务组件编写测试用例。因为一旦业务有了较大范围的调整,以前的测试用例很可能会遭到废弃,会浪费大量了人力和时间去重写。然而当业务逐渐趋于稳定时且团队资源丰富时,我认为为业务组件编写测试用例是一件较为必要的事。我们可以模拟不同状态下的请求来观察和断言我们数据的一致性,从而保证我们的组件上的数据是否符合预期。甚至可以达到在其他页面增删改时是否会影响到别的业务数据的正常流通。
  • 总结
    总而言之,我认为从长远看来,测试用例的撰写,无论是对于公共组件也好还是业务组件也好都是有一定深远的价值的。在项目愈发庞大之后越来越多的测试用例可以判断每一个功能的运行状态,这是一种更为直观的反馈问题的方式,也更方便以及更快捷地定位和暴露出一些在测试环节中无法展现的问题。发展地越好将更有利于项目整体的迭代难度。

介绍

为什么使用vue-test-utils
首先我们在上篇博客中说过,这款Vue测试的官方框架更有利于我们对于Vue实例以及页面DOM的抓取、事件的绑定快捷操作,下面贴一段原生的测试代码和使用后的代码让大家感受一下。

import Vue from 'vue'
import SelectMode from '@/views/Steps/SelectMode/SelectMode.vue'

describe('SelectMode.vue', () => {
  const Constructor = Vue.extend(SelectMode)
  const vm = new Constructor().$mount()
  const content = vm.$el.querySelectorAll('.one__item__right')
  const buttonGroupOne = content[0].querySelectorAll('button')
  const buttonGroupTwo = content[1].querySelectorAll('button')
  const buttonNextAttribute = vm.$el.querySelector('.btn__next').getAttribute('disabled')
  it('页面初始状态下各样式的状态', () => {
    expect(buttonGroupOne[0].className.indexOf('active') > -1).to.be.ok
    expect(buttonGroupOne[1].className.indexOf('active') == -1).to.be.ok
    expect(buttonGroupTwo[0].className.indexOf('active') == -1).to.be.ok
    expect(buttonGroupTwo[1].className.indexOf('active') > -1).to.be.ok
    expect(buttonNextAttribute == 'disabled').to.be.ok
  })
  it('其他按钮没有变化的情况下是否使用配置文件点击否,下一步按钮可点击', () => {
	  const clickEvent = new window.Event('click')
	  buttonGroupOne[1].dispatchEvent(clickEvent)
	  vm._watcher.run()
    expect(vm.$el.querySelector('.btn__next').getAttribute('disabled')).to.not.be.ok
  })
})

import Vue from 'vue'
import SelectMode from '@/views/Steps/SelectMode/SelectMode.vue'

describe('SelectMode.vue', () => {
  const Constructor = Vue.extend(SelectMode)
  const vm = new Constructor().$mount()
  const content = vm.$el.querySelectorAll('.one__item__right')
  const buttonGroupOne = content[0].querySelectorAll('button')
  const buttonGroupTwo = content[1].querySelectorAll('button')
  const buttonNextAttribute = vm.$el.querySelector('.btn__next').getAttribute('disabled')
  it('页面初始状态下各样式的状态', () => {
    expect(buttonGroupOne[0].className.indexOf('active') > -1).to.be.ok
    expect(buttonGroupOne[1].className.indexOf('active') == -1).to.be.ok
    expect(buttonGroupTwo[0].className.indexOf('active') == -1).to.be.ok
    expect(buttonGroupTwo[1].className.indexOf('active') > -1).to.be.ok
    expect(buttonNextAttribute == 'disabled').to.be.ok
  })
  it('其他按钮没有变化的情况下是否使用配置文件点击否,下一步按钮可点击', () => {
	  const clickEvent = new window.Event('click')
	  buttonGroupOne[1].dispatchEvent(clickEvent)
	  vm._watcher.run()
    expect(vm.$el.querySelector('.btn__next').getAttribute('disabled')).to.not.be.ok
  })
})

使用前

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

import SelectMode from '@/views/Steps/views/SelectMode/SelectMode.vue'

describe('SelectMode.vue', () => {
  const wrapper = shallowMount(SelectMode);
  // 可以通过wrapper.vm访问vue component的实例
  const vm: any = wrapper.vm
  const content = wrapper.findAll('.one__item__right')
  const itemWrapper = wrapper.findAll('.one__item__wrapper')
  const buttonGroupOne = content.at(0).findAll('button')
  const buttonGroupTwo = content.at(1).findAll('button')
  const buttonNextAttribute = wrapper.find('.btn__next').attributes().disabled
  it('页面初始状态下各样式的状态', () => {
    expect(buttonGroupOne.at(0).classes().indexOf('active') > -1).toBeTruthy();
    expect(buttonGroupOne.at(1).classes().indexOf('active') === -1).toBeTruthy();
    expect(buttonGroupTwo.at(0).classes().indexOf('active') === -1).toBeTruthy();
    expect(buttonGroupTwo.at(1).classes().indexOf('active') > -1).toBeTruthy();
    expect(buttonNextAttribute === 'disabled').toBeTruthy();
  });
  it('其他按钮没有变化的情况下是否使用配置文件点击否,下一步按钮可点击,是否安装系统是按钮禁用', () => {
    buttonGroupOne.at(1).trigger('click')
    Vue.nextTick(() => {
      expect(buttonGroupTwo.at(0).attributes().disabled).toBeTruthy();
      expect(buttonNextAttribute).toBeTruthy();
    })
  });
});

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

import SelectMode from '@/views/Steps/views/SelectMode/SelectMode.vue'

describe('SelectMode.vue', () => {
  const wrapper = shallowMount(SelectMode);
  // 可以通过wrapper.vm访问vue component的实例
  const vm: any = wrapper.vm
  const content = wrapper.findAll('.one__item__right')
  const itemWrapper = wrapper.findAll('.one__item__wrapper')
  const buttonGroupOne = content.at(0).findAll('button')
  const buttonGroupTwo = content.at(1).findAll('button')
  const buttonNextAttribute = wrapper.find('.btn__next').attributes().disabled
  it('页面初始状态下各样式的状态', () => {
    expect(buttonGroupOne.at(0).classes().indexOf('active') > -1).toBeTruthy();
    expect(buttonGroupOne.at(1).classes().indexOf('active') === -1).toBeTruthy();
    expect(buttonGroupTwo.at(0).classes().indexOf('active') === -1).toBeTruthy();
    expect(buttonGroupTwo.at(1).classes().indexOf('active') > -1).toBeTruthy();
    expect(buttonNextAttribute === 'disabled').toBeTruthy();
  });
  it('其他按钮没有变化的情况下是否使用配置文件点击否,下一步按钮可点击,是否安装系统是按钮禁用', () => {
    buttonGroupOne.at(1).trigger('click')
    Vue.nextTick(() => {
      expect(buttonGroupTwo.at(0).attributes().disabled).toBeTruthy();
      expect(buttonNextAttribute).toBeTruthy();
    })
  });
});

语法实战

Shallow Rendering

vue-test-utils中我们可以看到两种组件挂载的方式,分别为mountshallowMount。当我们使用mount挂载组件时会遇到一个问题,单元测试应该以独立的单位进行,也就是说,当我们测试App时,不需要也不应该关注其子组件的情况。这样才能保证单元测试的独立性。比如,在created钩子函数中进行的操作就会给测试带来不确定的问题。
为了解决这个问题,vue-test-utils提供了shallow方法,它和mount一样,创建一个包含被挂载和渲染的Vue组件的wrapper,不同的创建的是被存根的子组件。
这个方法可以保证你关心的组件在渲染时没有同时将其子组件渲染,避免了子组件可能带来的副作用(比如Http请求等)

DOM结构

挂载好Vue组件后,我们可以通过wrapper.vm访问vue component的实例
在我们抓取DOM时可以使用vue-test-utils中的findfindAll方法,与之对应的是querySelectorquerySelectorAll

一个选择器可以以一个css选择器,也可以选择一个Vue组件或者是查找一个对象

  • css选择器和原生的基本一致
  • 标签(divspanbar
  • 类(.class
  • 特性选择([type="text"]
  • id选择(#id
  • 伪类选择(div:first-child
  • 级联选择(div > .class
  • Vue组件选择:可以选择整个Vue组件
  • 查找选择对象
  • Name:可以根据组件的name选择元素。wrapper.find({ name: 'my-button' })
  • Ref: 可以根据$ref来选择元素。wrapper.find({ ref: 'myButton' })

findAll返回的是一个数组,在选择有多个元素的情况下是不可以使用find的,在使用findAll后需要使用at()来选择具体序列的元素。

在得到了我们的DOM元素之后我们就可以很方便地对属性以及内容进行断言判断。这里提一句,有关于样式的测试我更偏向于在E2E测试中去断言而不是在单元测试,这显得会更为直观,当然在单元测试中也提供了抓取class的API。
有关于DOM的API我罗列出了以下几个

  • attributes: 属性
  • classeswrapper.classes()返回一个字符串数组,wrapper.classes('bar')返回一个布尔值
  • contains:返回包含元素或组件匹配选择器
  • html: 以字符串形式返回DOM节点的HTML

测试Props

我们可以在组件挂载的时传入propsData

const wrapper = mount(Foo, {
  propsData: {
    foo: 'bar'
  }
})

const wrapper = mount(Foo, {
  propsData: {
    foo: 'bar'
  }
})

也可以使用setProps

这篇博客我们总结了一下关于测试用例的基本语法,下一篇我们会对于最重要的请求的mock以及组件对象中方法的mock进行一个详细的讲解。