UT 之前很少在前端写,但是最近业务需求,学习了一些关于Vue 2的Jest UT的方法,因为不涉及业务,所以没有系统的学习,
只是一些简单的入门经验总结,一些太深入的东西并没有做很多研究
,如果之后有新的收获,还会更新。
1. 先导入需要的组建
Vuex
: 本地状态存储,一本会存储一些全局数据和在action中进行一些ajax请求。VueRouter
:单文件Vue项目渲染页面和组建的路由地址。component
:需要被测试的组建。@vue/test-utils
:Vue 提供的模拟Vue实例
和生命周期hook函数
等的插件config
:使用config.mocks
,可以模拟一些,自定义在Vue.prototype
Vue实例原型上的全局变量,比如i18n
。createLocalVue
:在测试文件中模拟一下Vue实例。mount
: 模拟测试文件的挂载到Dom上的行为的函数,这个函数是深度挂载,会把组建component
内容和他所有的子组件都实例化加载之后进行测试,因此自然就会默认加载子组件中的created和mounted方法
,这样会使测试效率变低时间变长。- (3.1) 如果我们只需要关注某些子组件是否加载,而并不需要关注子组件内部的事件,我们可以使用
stubs在本地模拟这个组建
,这样在运行UT的时候,就不会加载子组件内部的状态和事件(这种情形一般是不常见的,因为使用mount挂载的时候,基本默认我们要加载所有的子组建了)
。 - (3.2) 如果我们的重点在于关注,父子组件中传值的逻辑,那么我们更建议使用
mount
,而不是shallowmount
。
- (3.1) 如果我们只需要关注某些子组件是否加载,而并不需要关注子组件内部的事件,我们可以使用
shallowMount
: 作用和mount
一致,但是这个是浅度的,只会渲染加载组建component
自身的内容,而他的子组件并不会被实例化渲染加载
,可以提高测试效率,相当于把组件中的所有子组件都默认进行了一次stabs
的模拟。在实际情况中,一个也页面可以存在大量的本地和全局的子组件,如果每个都进行一次stabs
,工作量太大。因此,一般建议使用shallowMount
。- (4.1) 但是很多情况下,在父组件中,需要直接使用
ref
调用子组件中的方法,但是子组件已经默认被stabs
了,子组件中的methods
自然也就无法调用了,一般会报子组件是个undefined
,此时,需要shallowmount
和stabs
结合使用,在本地重新模拟定义这个子组件中的方法methods
,让单元测可以继续进行即可。 - (4.2)
特殊的情况
: 如果父组件中的子组件是动态条件加载的
,此时使用shallowMount
无法加载这个子组件,测试这个子组件是否已经挂载时,就会报错。但是我们又不想关心这个子组件内部的事件和反应,此时我们就只能使用mount
和stabs
结合起来解决动态子组件无法加载的问题。
- (4.1) 但是很多情况下,在父组件中,需要直接使用
测试示例:
1. componentA.vue
html
<template>
<div>
<div class="title">{{classTitle}}</div>
<children-comp v-if="childrenList.length > 0" :list="childrenList" class="child-class" />
</div>
</template>
<script>
improt ChildrenComp from './ChildrenComponent';
import { cloneDeep } from 'lodash';
import { mapGetters } from 'vuex';
export default {
name:'componentA',
components: {
ChildrenComp
},
props: {
childrenDatas: {
type:Array,
default: () => []
}
},
computed: {
...mapGetters({
classNameList: 'classCode/getClassNameList'
}),
classTitle() {
if(this.$route.name === 'class1') {
return this.$t("compontA.classTitle1")
}
return this.$t("compontA.classTitle2")
},
childrenList() {
return cloneDeep(this.childrenDatas).map((child) => {
child.className = this.classNameList.find((class) => class.value === child.classCd)?.text || "";
return child;
})
}
}
}
</script>
2. componentA.spec.js
js
import Vuex from 'vuex';
import VueRouter from 'vue-router';
import subject from '@/components/componentA.vue';
improt ChildrenComp from '@/components/ChildrenComponent';
import { config, createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue(); // 创建测试文件本地Vue实例。
// 像main.js 一样要在本地Vue实例上注册Vuex和VueRouter。
localVue.use(Vuex);
localVue.use(VueRouter);
// 在运行之前,mocks模拟Vue.prototype实例原型上的自定义的方法等。
beforeAll(() => {
config.mocks.$t = (msg) => msg; // 相当于把原来的国际化,直接模拟成为一个自己想要的结果,避免无关紧要的函数的干扰。
});
// 测试文件整体描述:使用 describe() 函数
describe("ComponentA rendering test", () => {
// 设置需要模拟的实例。
let wrapper, // 接受mount挂载之后 返回的Vue实例对象,之后可以使用 wrapper.vm 调用所有挂载在Vue实例上的方法和数据
store, // 模拟 Vuex
router, // 模拟 VueRouter
propsData; // 模拟 传递父子组建传递的props 数据。
//在测试挂载开始之前,设置模拟的数据
beforeEach(async () => {
store = new Vuex.store({
modules: {
classCode: {
namespaced: true,
getters: {
getClassNameList: jest.fn().mockReturnValue([
{value: 'class1', text: '一班'},
{value: 'class2', text: '二班'},
]) // 如果遇到一些额外的函数请求,可以使用jest.fn()来模拟函数,然后返回我们可以用来进行测试的数据即可,在运行UT的时候,this.$store.getters['classCode/getClassNameList'] 返回的数据就是我们mock模拟的数据。
}
}
}
});
router = new VueRouter({
routes:[
{path:'/class1/detail',name:'class1'},
{path:'/class2/detail',name:'class2'},
]
});
propsData = {
childrenDatas: [] // 初始阶段设置 props 的数据为空。
};
wrapper = mount(subject, {
localVue,
store,
propsData,
router,
stubs: {
ChildrenComp:true
}
}); // 我们这种情况就适用于(4.2)的情况,因为这个子组建是条件加载的。如果子组件不是条件加载的,可以直接使用shallowMount
await wrapper.vm.$nextTick(); // 在父组件,子组件全部都调用挂载完成之后,再往下进行
});
afterEach(() => {
jest.resetAllMocks();
}); //页面测试结束,释放所有模拟的结果。
// 测试文件具体的测试用例的描述使用it() 函数
// 测试传递的props为空的测试,应该是 title 内部不一样,且子组件违背挂载。
it("test empty childrenDatas /class1/detail page is rendered", () => {
router.push({name : 'class1'}); // 测试不同路由时,需要进行router.push之后才能生效。
const title = wrapper.findAll(".title").at(0);
expect(title.text()).toBe("compontA.classTitle1");
expect(wrapper.findComponent("children-comp").exist()).toBeFalsy();
});
it("test empty childrenDatas /class2/detail page is rendered", () => {
router.push({name : 'class2'});
const title = wrapper.findAll(".title").at(0);
expect(title.text()).toBe("compontA.classTitle2");
expect(wrapper.findComponent("children-comp").exist()).toBeFalsy();
});
// 测试传递来的props不为空时的数据,子组件被挂载,且传递给子组件的数据被计算属性处理过。
it("test childrenDatas existed page is rendered", async () => {
wrapper.setProps({
childrenDatas: [{
classCd: 'class2',
name: 'testName',
gender: 'testGender'
}]
});//设置props 数据
await wrapper.vm.$nextTick(); // 在父组件,子组件全部都调用挂载完成之后,再往下进行。
const title = wrapper.findAll(".title").at(0);
expect(title.text()).toBe("compontA.classTitle2"); // 未设置router时,默认未"compontA.classTitle2";因为上面已经测试过路由的变化了,所以在这个用例里面,路由变化不是重点。
expect(wrapper.findComponent("children-comp").exist()).toBeTruthy(); // 当props,有数据时,子组件应该存在。
expect(wrapper.vm.childrenList).toEqual([{
classCd: 'class2',
name: 'testName',
gender: 'testGender',
className: '二班'
}]); //测试计算属性childrenList的数据是否
});
});
这样一个简单的componentA
和这个组件相关的UT
就写好了。
3 (4.1)
情况的简单示例:
- componentB.vue:
html
<template>
<div>
<childValidate ref="child">
...
</childValidate>
</div>
</template>
<script>
export default {
methods: {
save: () => {
this.$refs['child'].validate((valid) => {
if (valid) {
......
}
})
}
}
...
}
</script>
当使用
shallowMount
来浅度挂载componentB
时,子组件都没有被实例化挂载,当调用wrapper.vm.save()
时,this.$refs['child']
就是undefined
,导致报错,进而使整个单元测无法进行。那么此时就需要在shallowMount
中的stabs对象
中,模拟一个childValidate 组件
。如下:
- componentB.spec.js
js
...
// 子组件的模拟对象
const childValidate = {
render: jest.fn(), // 给这个子组件的渲染函数模拟一个函数并且没有返回值,表示此组件并没有实际渲染的必要
methods: {
validate: (cb) => {cb(true)} // 模拟子组件上的 methods validate,并返回true,让校验通过继续,使测试文件继续进行。
}
};
wrapper = shallowMount(componentB, {
localVue,
store,
router,
propsData,
stubs: {
childValidate // 模拟组件 childValidate
}
})
...
给不需要被关注的
子组件 childValidate
模拟一个组件对象
,永远返回校验正确
的结果,当调用wrapper.vm.save()
时,使UT
可以跑通,达到验证测试的实际目的。
4. 总结:
最开始写
前端UT
的时候,确实无从下手。后来发现其实不管时前端还是后端的单元测试,一个核心的目标就是,测到我们期望的数据变化和结果,再实现这个目标的过程中,遇到无需关注却复杂的全局原型变量,ajax请求的函数和一些复杂纷乱的组件等,分别可以使用config.mocks
,jest.fn
,stabs
等本地模拟的方式代替,最终获取我们想要测试以及期望的数据即可,后续还学到什么知识,依然会继续更新。