前言
全方位的介绍如何使用JEST测试一个VUE组件。
(如果不知道怎么开始VUE单元测试的同学们,请查看之前的文章VUE单元测试--开启测试之旅 (opens new window))
着重介绍在使用Vue.extend创建构造函数的形式注册的组件,包括:
- 测试定时器函数
- 测试
HTTP请求 - 测试事件 等这几个部分的介绍
代码在github (opens new window)欢迎点赞👍
测试组件
测试组件,其实就是测试组件的方法以及方法所依赖的模块。
测试组件方法很简单:调用组件方法并断言方法能用正确地影响了组件的输出即可。
从一个例子出发,测试一个进度条组件
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代码一目了然,当调用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');
})
})
2
3
4
5
6
7
8
9
10
11
12
运行yarn test:unit时,测试通过。
这是简单的组件测试。
Vue实例添加属性
事实上还有一种非常常见的组件模式,就是往Vue实例添加属性。在之前的文章中也介绍过,用这VUE的“动态”案例 (opens new window)介绍的组件来做例子,组件的具体开发就不多作介绍了,代码在代码在github (opens new window)中。
(进入到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)
})
})
2
3
4
5
6
7
8
9
运行yarn test:uni时会发生错误
这是因为在测试中直接挂载了组件,而这个组件实例是使用Vue.extend函数创建的,并在main.js引入和添加到Vue的原型中的。换而言之,main.js并没有被执行,这个组件就没有被创建,$alarm属性就永远不会被添加。
这时需要在加载组件到测试之前先为Vue实例添加属性。可以使用mocks来实现。
shallowMount(Home, {
mocks: {
$alarm: () => {}
}
})
2
3
4
5
再次运行yarn test:uni时就完美的通过了。
测试定时器函数
定时器函数包括JavaScript异步函数都是前端中常见的功能,所以都需要测试对应的代码。但肯定不是等待定时器函数走完,需要使用Jest.useFakeTimers替换全局定时器函数,替换后可以使用runTimersToTime推进时间。
测试setTimeout
功能:handleCheck事件触发后,会将alarm组件id unshift到idList数组中,弹出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);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
测试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)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
测试模拟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('人机'));
2
然后在测试文件中加入
// alarm.spec.js
jest.mock('../../src/api/alarmApi.js');
2
调用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('人机')
})
2
3
4
5
6
设置断言数量是在异步测试中非常常用的方法,因为这可以确保在测试结束前执行完所有断言。
在组件中调用HTTP请求函数
一般来说,HTTP请求是在组件中使用的,单独测试作用并不大。那么在组件中载入其他异步依赖,应该怎么去测试呢?
首先在home.vue中导入请求文件
import * as alarmApi from '@/api/alarmApi.js';
// 在handleCheck中使用
async handleCheck () {
// ...
const name = await alarmApi.fetchAlarmDetail();
// ...
}
2
3
4
5
6
7
8
当测试调用异步代码的时候,并不总是可以访问需要等待的异步函数。这意味着不能在测试中使用await来等待异步函数结束。
这时可以使用flush-promises库来帮忙,它能等待异步函数结束。例如:
let loading = true;
Promise.resolve().then(() => {
loading = false;
}
await flushPromise();
expect(loading).toBe(false)
2
3
4
5
6
基于此,将之前的测试用例修改为:
// 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)
})
2
3
4
5
6
7
8
9
10
11
12
13
加入一个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();
})
2
3
4
5
6
7
每个包装器都有一个trigger方法,用于在包装器上分发一个事件。
// 键盘事件
wrapper.trigger('keydown.up');
wrapper.trigger('keydown', {
key: 'a'
})
// 鼠标事件
wrapper.trigger('mouseenter');
2
3
4
5
6
7
测试自定义事件
VUE自定义事件是由带有VUE实例$emit方法的组件事件发射出去的。在子组件中发射一个事件:
// son.vue
this.$emit('eventName', payload);
2
在父组件中接收一个事件:
// father
<son @eventName='handleEvent'></son>
2
功能: 点击位于HelloWorld组件的class为hello的button元素,触发sayHello事件并携带hello。位于Home组件的handleSayHello触发,将greeting从hi变成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')
})
2
3
4
5