VUE单元测试--进阶之路

1,372 阅读5分钟

前言

全方位的介绍如何使用JEST测试一个VUE组件。
(如果不知道怎么开始VUE单元测试的同学们,请查看之前的文章VUE单元测试--开启测试之旅

着重介绍在使用Vue.extend创建构造函数的形式注册的组件,包括:

  • 测试定时器函数
  • 测试HTTP请求
  • 测试事件 等这几个部分的介绍

代码在github欢迎点赞👍

测试组件

测试组件,其实就是测试组件的方法以及方法所依赖的模块。
测试组件方法很简单:调用组件方法并断言方法能用正确地影响了组件的输出即可。

从一个例子出发,测试一个进度条组件

<template>
  <div class="Progress-Bar" :class="{hidden: hidden}">
  </div>
</template>

<script>
export default {
  name: 'ProgressBar',
  data () {
    return {
      hidden: true
    }
  },
  methods: {
    start () {
      this.hidden = false;
    },
    finally () {
      this.hidden = true;
    }
  },
}
</script>

代码一目了然,当调用start方法时,应该展示进度条;当调用finally方法时,应该屏蔽进度条。

import { shallowMount } from '@vue/test-utils';
import ProgressBar from '@/views/ProgressBar.vue';

describe('test progress', () => {
  it('when start is clicked, show the progressBar', () => {
    const wrapper = shallowMount(ProgressBar);
    expect(wrapper.classes()).toContain('hidden');
    wrapper.vm.start();
    wrapper.vm.finally();
    expect(wrapper.classes()).toContain('hidden');
  })
})

运行yarn test:unit时,测试通过。

这是简单的组件测试。

Vue实例添加属性

事实上还有一种非常常见的组件模式,就是往Vue实例添加属性。在之前的文章中也介绍过,用这VUE的“动态”案例介绍的组件来做例子,组件的具体开发就不多作介绍了,代码在代码在github中。
(进入到unitest文件夹中,运行yarn serve。)

功能:点击按钮触发handleCheck事件,弹出alarm弹窗,弹窗id就是在primaryId基础上增加1。
测试用例如下:

describe('test alarm', () => {
  it('when handleCheck is clicked, show the alarm', () => {
    const wrapper = shallowMount(Home);
    const count = wrapper.vm.primaryId;
    wrapper.vm.handleCheck();
    const newCount = wrapper.vm.primaryId;
    expect(newCount).toBe(count + 1)
  })
})

运行yarn test:uni时会发生错误

这是因为在测试中直接挂载了组件,而这个组件实例是使用Vue.extend函数创建的,并在main.js引入和添加到Vue的原型中的。换而言之,main.js并没有被执行,这个组件就没有被创建,$alarm属性就永远不会被添加。

这时需要在加载组件到测试之前先为Vue实例添加属性。可以使用mocks来实现。

shallowMount(Home, {
  mocks: {
    $alarm: () => {}
  }
})

再次运行yarn test:uni时就完美的通过了。

测试定时器函数

定时器函数包括JavaScript异步函数都是前端中常见的功能,所以都需要测试对应的代码。但肯定不是等待定时器函数走完,需要使用Jest.useFakeTimers替换全局定时器函数,替换后可以使用runTimersToTime推进时间。

测试setTimeout

功能:handleCheck事件触发后,会将alarm组件id unshiftidList数组中,弹出3秒后组件就会被销毁,idList也会将其id给删除掉。
测试用例如下:

it('when handleCheck is clicked, 3second later alarm would be disappeared', () => {
  // 测试之前,替换全局定时函数
  jest.useFakeTimers();
  const wrapper = shallowMount(Home, {
    mocks: {
      $alarm: () => {}
    }
  });
  wrapper.vm.handleCheck();
  expect(wrapper.vm.idList.length).toBe(1);
  // 将时间推进3000毫秒
  jest.runTimersToTime(3000);
  expect(wrapper.vm.idList.length).toBe(0);
})

测试clearTimeout

功能:当alarm弹窗超过一个的时候,就会调用clearTimeout销毁前一个的timer。这时就要监听clearTimeout是否被调用。

使用Jest.spyOn函数创建一个spy,可以使用toHaveBeenCalled匹配器来检测spy是否被调用,更进一步地可以使用toHaveBeenCalledWith匹配器测试spy是否带有指定参数被调用。

所以在测试中,需要得到setTimeout的返回值,Jest.mockReturnValue可以实现这个需求。mockReturnValue可以将setTimeout的返回值设置为任何值,比如将返回值设置为123:setTimeOut.mockReturnValue(123)
测试用例如下:

it('when handleCheck is clicked and the number of alarm exceeds 1 , The previous alarm disappears immediately', () => {
    // 监听clearTimeout
    jest.spyOn(window, 'clearTimeout')
    const wrapper = shallowMount(Home, {
      mocks: {
        $alarm: () => {}
      }
    });
    // 设置setTimeout返回值为123
    setTimeout.mockReturnValue(123)
    wrapper.vm.handleCheck();
    // 设置setTimeout返回值为456
    setTimeout.mockReturnValue(456)
    wrapper.vm.handleCheck();
    expect(window.clearTimeout).toHaveBeenCalledWith(123)
  })

测试模拟HTTP请求

HTTP请求不在单元测试范围,因为它会降低单元测试的速度;降低单元测试的可靠性,因为HTTP请求不会100%请求成功。所以需要在单元测试中模拟api文件,从而使得fetchAlarmDetail永远不会发送一个HTTP请求。

Jest提供了一个API,用于选择当一个模块导入另一个模块时返回哪些文件或函数。首先的创建一个mock文件,而不是直接在测试中引入真正的文件。

api目录中创建一个__mocks__目录,里面创建一个模拟的需要测试的alarmApi文件

// src/api/__mocks__/alarmApi.js
export const fetchAlarmDetail = jest.fn(() => Promise.resolve('人机'));

然后在测试文件中加入

// alarm.spec.js
jest.mock('../../src/api/alarmApi.js');

调用jest.mock('../../src/api/alarmApi.js')后,当模块导入了src/api/alarmApi.js后,Jest将使用创建的mock文件而不是原文件。

测试用例如下:

it('fetch the alarm detail by http', async () => {
  // 设置断言数量,如果一个promise被拒绝,测试会失败
  expect.assertions(1);
  const name = await alarmApi.fetchAlarmDetail();
  expect(name).toBe('人机')
})

设置断言数量是在异步测试中非常常用的方法,因为这可以确保在测试结束前执行完所有断言。

在组件中调用HTTP请求函数

一般来说,HTTP请求是在组件中使用的,单独测试作用并不大。那么在组件中载入其他异步依赖,应该怎么去测试呢?

首先在home.vue中导入请求文件

import * as alarmApi from '@/api/alarmApi.js';

// 在handleCheck中使用
async handleCheck () {
    // ...
    const name = await alarmApi.fetchAlarmDetail();
    // ...
}

当测试调用异步代码的时候,并不总是可以访问需要等待的异步函数。这意味着不能在测试中使用await来等待异步函数结束。

这时可以使用flush-promises库来帮忙,它能等待异步函数结束。例如:

let loading = true;
Promise.resolve().then(() => {
    loading = false;
}
await flushPromise();
expect(loading).toBe(false)

基于此,将之前的测试用例修改为:

// 2.1
  it('when handleCheck is clicked, show the alarm', async () => {
    expect.assertions(1);
    const wrapper = shallowMount(Home, {
      mocks
    });
    const count = wrapper.vm.primaryId;
    alarmApi.fetchAlarmDetail.mockImplementationOnce(() => Promise.resolve('人机'));
    wrapper.vm.handleCheck();
    await flushPromises();
    const newCount = wrapper.vm.primaryId;
    expect(newCount).toBe(count + 1)
  })

加入一个expect.assertions(1)设置断言数量,设置fetchAlarmDetail函数的返回结果,最后调用await flushPromises();等待所有异步函数结束。(之后的测试用例修改,不再展开讨论,详情请看代码。)

在命令行中输入yarn test:uni

测试事件

测试DOM事件

功能:点击一个按钮,触发一个click事件 测试用例:

it('click the button then the $alarm will be called', () => {
  const wrapper = shallowMount(Home, {
    mocks
  });
  wrapper.find('button.check').trigger('click');
  expect($alarm).toHaveBeenCalled();
})

每个包装器都有一个trigger方法,用于在包装器上分发一个事件。

// 键盘事件
wrapper.trigger('keydown.up');
wrapper.trigger('keydown', {
  key: 'a'
})
// 鼠标事件
wrapper.trigger('mouseenter');

测试自定义事件

VUE自定义事件是由带有VUE实例$emit方法的组件事件发射出去的。在子组件中发射一个事件:

// son.vue
this.$emit('eventName', payload);

在父组件中接收一个事件:

// father
<son @eventName='handleEvent'></son>

功能: 点击位于HelloWorld组件的classhellobutton元素,触发sayHello事件并携带hello。位于Home组件的handleSayHello触发,将greetinghi变成hello

it('click the check button, home.greeting will change to hello', () => {
    const wrapper = shallowMount(Home);
    wrapper.findComponent(Hello).vm.$emit('sayHello', 'hello');
    expect(wrapper.vm.greeting).toBe('hello')
  })

结尾

阅读完,如果觉得有帮助的请点点赞,支持一下。

更多文章请移步楼主github,如果喜欢请点一下star,对作者也是一种鼓励。