Vue 2 & Jest Test 总结

UT 之前很少在前端写,但是最近业务需求,学习了一些关于Vue 2的Jest UT的方法,因为不涉及业务,所以没有系统的学习,只是一些简单的入门经验总结,一些太深入的东西并没有做很多研究,如果之后有新的收获,还会更新。

1. 先导入需要的组建

  • Vuex: 本地状态存储,一本会存储一些全局数据和在action中进行一些ajax请求。
  • VueRouter:单文件Vue项目渲染页面和组建的路由地址。
  • component:需要被测试的组建。
  • @vue/test-utils:Vue 提供的模拟Vue实例生命周期hook函数等的插件
    1. config:使用config.mocks,可以模拟一些,自定义在Vue.prototypeVue实例原型上的全局变量,比如i18n
    2. createLocalVue:在测试文件中模拟一下Vue实例。
    3. mount: 模拟测试文件的挂载到Dom上的行为的函数,这个函数是深度挂载,会把组建component内容和他所有的子组件都实例化加载之后进行测试,因此自然就会默认加载子组件中的created和mounted方法,这样会使测试效率变低时间变长。
      • (3.1) 如果我们只需要关注某些子组件是否加载,而并不需要关注子组件内部的事件,我们可以使用stubs在本地模拟这个组建,这样在运行UT的时候,就不会加载子组件内部的状态和事件(这种情形一般是不常见的,因为使用mount挂载的时候,基本默认我们要加载所有的子组建了)
      • (3.2) 如果我们的重点在于关注,父子组件中传值的逻辑,那么我们更建议使用mount,而不是shallowmount
    4. shallowMount: 作用和mount一致,但是这个是浅度的,只会渲染加载组建component自身的内容,而他的子组件并不会被实例化渲染加载,可以提高测试效率,相当于把组件中的所有子组件都默认进行了一次stabs的模拟。在实际情况中,一个也页面可以存在大量的本地和全局的子组件,如果每个都进行一次stabs,工作量太大。因此,一般建议使用shallowMount
      • (4.1) 但是很多情况下,在父组件中,需要直接使用ref调用子组件中的方法,但是子组件已经默认被stabs了,子组件中的methods自然也就无法调用了,一般会报子组件是个undefined,此时,需要shallowmountstabs结合使用,在本地重新模拟定义这个子组件中的方法methods,让单元测可以继续进行即可。
      • (4.2) 特殊的情况: 如果父组件中的子组件是动态条件加载的,此时使用shallowMount无法加载这个子组件,测试这个子组件是否已经挂载时,就会报错。但是我们又不想关心这个子组件内部的事件和反应,此时我们就只能使用mountstabs结合起来解决动态子组件无法加载的问题。

测试示例:

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等本地模拟的方式代替,最终获取我们想要测试以及期望的数据即可,后续还学到什么知识,依然会继续更新。

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
2401_8576009511 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_8576009511 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL11 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
轻口味11 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_8575834911 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js
java_heartLake12 小时前
Vue3之性能优化
javascript·vue.js·性能优化
ddd君3177413 小时前
组件的声明、创建、渲染
vue.js
前端没钱14 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js