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

相关推荐
2401_868534781 小时前
常见的 vue面试题目
前端·javascript·vue.js
向日的葵0062 小时前
vue路由(二)
前端·javascript·vue.js·vue
xkxnq3 小时前
第八阶段:工程化、质量管控与高级拓展(130天),Vue端到端测试:Cypress自动化测试(登录流程+表单提交+页面跳转)
前端·vue.js·flutter
老毛肚3 小时前
jeecgboot vue API 拆分02
前端·javascript·vue.js
爱因斯坦乐14 小时前
Vue项目整合
前端·javascript·vue.js
ct97815 小时前
组件间的通信
前端·javascript·vue.js
左手吻左脸。15 小时前
Vue 全栈面试题大全(2026 最新版最详细)
前端·javascript·vue.js
小新11016 小时前
最简单但完整的 Vue 响应式示例(一个简单的计数器按钮)
前端·javascript·vue.js
刘海不能乱1617 小时前
Java JUC源码分析系列笔记-Synchronized
vue.js
whatever who cares19 小时前
Vue3中vue文件和composables的分工
前端·javascript·vue.js