Vue组件中的单元测试

1,160 阅读7分钟

Vue组件的单元测试

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

但是其实单元测试就是针对程序的最小单元来进行正确性验证的测试工作,由于Vue组件中的单文件组件的出现,其实给单元测试提供了更加方便的方式测试。通俗来讲,单元测试就是为了测试某一个类、某个单个程序、类、方法等快速定位bug、减少调试时间、放心重构

使用单元测试的原因

1)分模块开发,方便定位到哪个单元出了问题
2)保证代码质量
3)驱动开发
    先写测试目标-->再开发功能-->然后运行测试看是否符合测试目标-->不符合改

单元测试类型

TDD:
    测试驱动开发,从需求角度看,即我需要结果是什么,如果不是就是错误的,
    需求分析->填写代码使单元测试通过->重构
BDD
    行为驱动开发,从具体功能角度出发看。即结果应该是什么,如果不是什么就出错,
    从业务角度定义目标->找到实现目标方法->编写单元测试->实现行为->检查产品

单元测试的目的

当自己写的项目足够大的时候,在得加模块和组件的过程中,可能会对之前模块或者是组件造成一定的影响,但是被影响的模块已经通过了测试,我们在迭代的时候,很少有测试人员重新对之前的代码进行再一次的测试,所以,在项目部署的过程中就会出现隐形的bug,所以我们在测试的时候采用的是自动化测试。最主要的是对于大型项目,在每次迭代的时候,可以保证整个系统的正确运行。

单元测试的核心内容

测试框架
    Jest-基于jasmine,对react友好
    jasmine-bdd风格,自带assert(断言库)和mock
    Mocha-全面适合node 和浏览器两个端
    Qunit-出自jQuery后来独立出来
    
断言库
    chai-支持所有风格-全面
    Should
    expect
    Assert-node环境直接使用
Mock库
    sinom
Test runner
    karma
覆盖率工具
    istanbul

测试脚本

Mocha是现在最流行的JavaScript测试框架之一,该测试框架,在浏览器和Node环境都可以使用,所谓的测试框架就是运行测试的工具,通过它可以为JavaScript应用添加测试,从而保证代码质量

// add.test.js
var add = require('./add.js');
var expect = require('chai').expect;

describe('加法函数的测试', function() {
  it('1 加 1 应该等于 2', function() {
    expect(add(1, 1)).to.be.equal(2);
  });
});

上述代码就是一段测试脚本,可以独立运行,测试脚本应该里面包括一个或多个describe块,每个describe块应该包括一个或多个it块

describe块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。

it块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1 加 1 应该等于 2"),第二个参数是一个实际执行的函数。

断言库的使用

断言其实就是判断源码实际执行结果与预期结果是否一致,如果不一致就抛出一个错误,所有的测试用例(it块)都应该包含一句或多句断言,他是编写测试用例的关键,断言功能由断言库来实现,Mocha本身不带有断言库,我们这里使用的是chai断言库,并且指定使用它的expect断言风格,其中expect的断言语法很接近自然语言,如下面的一些例子

// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });

// 布尔值为true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);

// include
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);

如果断言不成立就会抛出一个错误,所以说只要没有报错,测试用例就算通过

具体使用步骤

1.创建一个vue的单文件组件

<template>
    <div>
      {{ count }}
      <button @click="increment">自增</button>
    </div>
</template>
<script>
export default {
  data () {
    return {
      count: 0
    }
  },
  methods: {
    increment () {
      this.count++
    }
  }
}
</script>

2.对组件进行测试,验证数据是否正确

import Vue from 'vue'
import MyComponent from '@/components/MyComponent.vue'

describe('MyComponent.vue', () => {
  it('计数器在点击按钮时自增', () => {
    // 获取mount中的组件实例
    const vmComponent = new Vue(MyComponent).$mount();
    // 点击之前
    console.log('计数器点击之前的值:' + vmComponent.count);
    // 调用实例中的increment方法,点击计数器
    vmComponent.increment();
    // 点击之后
    console.log('计数器点击之后的值:' + vmComponent.count);
    // 判断最后的count是否为最后对应的值
    expect(vmComponent.count).toBe(1);
  })
})

3.使用npm run init运行

上图是运行成功的状态图 如果把1-->2,就会出现错误,比如说下面的状态图

以上就是对单元测试流程的大概简述。

关于nextTick的使用

补充nextTick的使用

vm.msg='Hello'
    DOM还没有更新
Vue.nextTick(function(){
    DOM更新了
})

参数:Vue.nextTick( [callback, context] )
用法:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,
获取更新后的DOM

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })
  
  如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。
  请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持
  Promise (IE:你们都看我干嘛),你得自己提供 polyfill。

在单元测试中,我们也可以使用Vue.nextTick()方法,由于在Vue中会异步的将未生效的DOM更新批量应用,以避免因数据反复突变而导致的无谓的重渲染,所以这里我们使用了Vue.nextTick()方法来完成DOM等待更新,但是在单元测试中为了简化用法,Vue Test Utils 同步应用了所有的更新,所以你不需要在测试中使用 Vue.nextTick 来等待 DOM 更新

注意:但是当你需要为异步回调或Promise解析等操作明显改进事件循环的时候,
nextTick仍然是必要的

但是在单元测试中使用vue.nextTick(),在任意的内部抛出错误可能都不会被测试运行器捕获,因为内部使用了Promise。

解决方法就是:

1)在测试的一开始就将Vue的全局错误处理器设置为done回调,
it('will catch the error using done', done => {
    //这种方式在一开始就将Vue的全局错误处理其设置为done
      Vue.config.errorHandler = done
      Vue.nextTick(() => {
        expect(true).toBe(false)
        done()
      })
    })
2)你可以在调用nextTick时不带参数让其作为一个promise返回
it('will catch the error using a promise', () => {
//返回一个promise
  return Vue.nextTick().then(function() {
    expect(true).toBe(false)
  })
})

单元测试的使用

测试同步方法
   describe('Counter.vue', () => {
      it('increments count when button is clicked', () => {
        const wrapper = shallowMount(Counter)
        wrapper.find('button').trigger('click')
        expect(wrapper.find('div').text()).contains('1')
      })
})
测试异步方法
API的调用和Vuex action 都是最常见的异步行为之一
假设当前都一个异步的API接口
    export default {
         get: () => Promise.resolve({ data: 'value' })
    }
失败的测试用例
    it('fetches async when a button is clicked', done => {
      const wrapper = shallowMount(Foo)
      wrapper.find('button').trigger('click')
      wrapper.vm.$nextTick(() => {
        expect(wrapper.vm.value).toBe('value')
        done()
      })
    })

上面的测试用例是失败的,由于js的执行机制问题,所以断言在 fetchResults 中的 Promise 完成之前就被调用了,Jest 和 Mocha 都提供一个回调来使得运行期知道测试用例的完成时机的方法,他们使用的都是 done方法。对于这种情况,我们可以和 $nextTick 或 setTimeout 结合使用 done 来确保任何 Promise 都会在断言之前完成。 正确的测试用例写法:

it('fetches async when a button is clicked', done => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  wrapper.vm.$nextTick(() => {
    expect(wrapper.vm.value).toBe('value')
    done()
  })
})

对于这种异步回调的方法,我们也可以对其做简化,采用的是ES7中的async和await来实现异步等待,这里搭配的是flush-promises包,这个包的作用就是清除所有等待完成的Promise具柄

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

it('fetches async when a button is clicked', async () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  await flushPromises()
  expect(wrapper.vm.value).toBe('value')
})

单元测试常见的API

mount()

参数:单个组件
返回值:{wrapper}

使用:创建一个包含被挂载和渲染的Vue组件的wrapper

使用Vue选项
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = mount(Foo, {
      propsData: {
        color: 'red'
      }
    })
    expect(wrapper.props().color).toBe('red')
  })
})
固定在DOM上
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = mount(Foo, {
      attachToDocument: true
    })
    expect(wrapper.contains('div')).toBe(true)
  })
})
默认插槽和具名插槽
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
import FooBar from './FooBar.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = mount(Foo, {
      slots: {
        default: [Bar, FooBar],
        fooBar: FooBar, // 将匹配 `<slot name="FooBar" />`。
        foo: '<div />'
      }
    })
    expect(wrapper.contains('div')).toBe(true)
  })
})
将全局属性存根
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const $route = { path: 'http://www.example-path.com' }
    const wrapper = mount(Foo, {
      mocks: {
        $route
      }
    })
    expect(wrapper.vm.$route.path).toBe($route.path)
  })
})
将组件存根
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
import Faz from './Faz.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = mount(Foo, {
      stubs: {
        Bar: '<div class="stubbed" />',
        BarFoo: true,
        FooBar: Faz
      }
    })
    expect(wrapper.contains('.stubbed')).toBe(true)
    expect(wrapper.contains(Bar)).toBe(true)
  })
})

shallowMount()

这个跟上面的mount()一样,创建一个包含被挂载和渲染的Vue组件的wrapper,不同的是被存根的子组件

render

将一个对象渲染成为一个字符串并返回一个包裹器

renderToString()

将一个组件渲染为HTML api详解参考 vue-test-utils.vuejs.org/zh/api/#ren…

选择器

vue-test-utils.vuejs.org/zh/api/#选择器

provide和inject的区别

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。

inject 选项应该是:

一个字符串数组,或
一个对象,对象的 key 是本地的绑定名,value 是:
  • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
  • 一个对象,该对象的:
    • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
    • default 属性是降级情况下使用的 value