网上关于vue-test-utils+jest的相关文章很多,大致可以分为两大流派:配置构建派的特殊问题。对于想要实现单元测试的入门级开发者来说,有一个缺失的环节:面对特定场景如何编写单元测试代码。本文列出了一些基于不同场景的真实代码示例。希望它能帮助那些想要真正实施单元测试的团队。Mock实例方法场景示例在submit方法中,一般会调用验证方法,依赖returntrue||验证方法的false来判断是否执行实际的提交逻辑。但有时很难构造在需要测试的方法中调用的其他方法。比如验证方法需要传递几个参数,所有的参数都必须符合才能继续执行提交的代码逻辑。那么这个时候直接让验证方法通过比较好。当然,这又引出了另一个话题,我们在进行单元测试时是否应该一起测试引用方法呢?我的看法是,既然是单元测试,引用方法不一定非要覆盖,原因如下:TDD过程中要测试的方法和里面的引用方法不一定是同一个人负责单元测试应该只是对当前测试方法的逻辑是否正确负责。比如提交方式需要考虑提交成功或失败的各种场景,验证方式要通过自己的单元测试代码来保证验证本身是正确的。因此,在这种场景下,我们对于特别复杂的引用方法,可以考虑mocking。代码如下jest.spyOn(wrapper2.vm,'validate').mockImplementation(()=>{returntrue})wrapper2.vm.handleSubmit()//如果直接调用submit方法,验证方法会直接返回success,这样我们就可以直接测试提交里面的方法了。例如,对于特定字段的断言场景,有时逻辑代码中某个方法调用的参数可能非常大,但对于某个测试场景,我们只需要判断参数是否包含某些特定的字段或值即可。比如我们调用一个方法,代码如下get(params)}在写单元测试的时候,我们需要验证的是,在某种场景下,params中必须包含keyJ=J的参数值,此时,我们不需要assert整个params对象让params={key1:value1,key2:value2,key3:value3,...keyn:valuen}//上面手动构建了完整的参数对象,不要做expect(request.get).toBeCalledWith(params)如果你只关心大对象中某个字段的值,不要像上面那样写测试代码,请参考下面的测试代码示例expect(request.get).toBeCalledWith(expect.objectContaining({key3:42//假设我们搭建的测试场景只需要特别注意这个场景中的key3值,这个值必须是42}))关于refs的处理,业务代码包括调用currentcomponentrefs来引用该组件,而调用approach时应该如何处理呢?考虑以下场景方法:{save:()=>{this.$refs[name].validate((valid)=>{if(valid){......}})}}我们应该如何保存编写测试代码的方法?如果使用vuetestutils的shallowMount方法,嵌套的组件实际上不会在测试中被实例化。例如,在上面的代码中,这里引用的refs指向了Form组件。如果在测试中直接执行wrapper.vm.save()会报错,显示调用了未定义的validate方法。这时候就需要在shallowMountconstForm={render:jest.fn(),methods:{validate:(cb)=>{cb(true)}}}wrapper=期间通过stubs属性模拟加载一个嵌入式组件shallowMount(indexTip,{localVue,store,propsData:{},stubs:{Form}})首先声明需要存根的Form组件。这个新声明的组件只是用来模拟测试中可能需要的行为,避免造成未定义的异常,所以只需要声明必要的方法即可。render方法直接声明为jest.fn(),因为在任何htmlmethods中声明的validate方法都不需要真正渲染,这里的声明在测试代码中实际会被调用。请注意,存根的方法声明无论如何都会返回true。还是按照上面说的原则,单元测试只测试与当前功能相关的逻辑。在shallowMount的参数中,使用stubs属性声明一个自定义的Form。这样,再次调用wrapper.vm.save方法时,就不会报validate方法未定义了。注意:业务代码中调用了refs.xxxname,其中xxxnames为组件元素在模板中的ref名称。但是stub的时候需要声明的是组件的名字。比如上面的例子,业务代码中的refname是xxxname,组件名称是Form。这时候stub需要模拟的是Form,而不是xxxname。父组件模拟与上述场景相反。本次讨论如果业务代码中有调用父组件的情况下,如何编写测试代码。this.$parent.getList()虽然我觉得直接调用parent方法不是一个好的做法,但是谁让vue提供了这样的能力,你也不能保证没有人会这样用。该场景的测试代码,原理与上述场景类似,只是在shallowMount的调用上稍有不同。看具体代码constParent={data:()=>({val:true}),methods:{getList:()=>{}//当然如果业务代码需要依赖returngetList方法,这里也可以返回结果},template:'
'}//然后在shallowMount的时候声明父组件即可wrapper=shallowMount(indexTip,{parentComponent:Parent,//声明父组件如上面定义的ParentlocalVue,store,propsData:{}})mock异步请求在测试中,我们经常会遇到需要针对不同的异步请求结果编写测试代码的情况。比如接口返回code:0怎么办,code:1怎么办,甚至网络函数中请求失败怎么办fn(xxx){returnaxios.get(url,{id:xxx}).then(res=>{if(res.code===10000){this.list=res.list}else{this.fail(res.errmsg)}}).catch(err=>{this.toast(err.message)})}如上面的代码,我们要测试的函数叫做fn,它调用了一个异步请求,那么我们需要在测试代码中获取这个异步Promise对象来编写测试代码它的内部逻辑,所以fn函数需要返回这个Promise对象。测试代码如下那个时候的vm.list已经赋值成功})进一步思考,既然是单元测试,是不是不应该依赖接口的返回,比如需要测试前端代码的逻辑处理当没有网络连接时,我们不能在持续集成过程中突然拔掉网线吧。所以我们还需要使用jest提供的mockpromise能力来mockPromise对象的返回值。上面介绍了mockPromise对象的方法,可以让我们在不实际进行网络调用的情况下触发相应的处理逻辑。但大多数情况下,更细分的是在不同的代码分支上做断言。就像if(res.code===10000)...elseif(res.code===10002){if(res.hasSelected===true)...else{...}}因此,jest提供快速模拟不同返回值的方法axios.get.mockResolvedValue({code:10000,list:[1,2,3]})//mock成功状态awaitwrapper.vm.fn()expect(wrapper.vm.list.length).toBe(3)//断言fn内部promise调用成功时,list属性应该是三元素数组axios.get.mockResolvedValue({code:10001,msg:'idnotfountain'})//模拟没有找到id对应的记录awaitwrapper.vm.fn()expect(wrapper.vm.fail).toBeCalledWith('idnotfound')//assertfn在这个场景中,fail方法被调用,参数为'idnotfound'axios.get.mockRejectedValue('networkError')//在reject的情况下awaitwrapper.vm.getLouDong()expect(wrapper.vm.toast).toBeCalledWith('networkError')mocksetTimeout业务代码,可以在方法内部执行setTimeout,然后p一定时间后执行一些操作,比如submit(){this.toast('clearthesky')setTimeout(()=>{this.loaded=[]},300)}这个场景可能是针对用户经验的考虑。那么如果按照常规的方法,我们在执行完getNewList方法后立即进行断言,测试肯定会失败。针对这种情况,jest提供了一个有用的函数,useFakeTimers。在测试代??码中,只需要声明jest.useFakeTimers,然后调用相应的方法,让假定时器向前或向后移动几毫秒即可。请参阅代码it('提交测试用例',()=>{jest.useFakeTimers()wrapper.vm.submit()jest.advanceTimersByTime(350)expect(wrapper.vm.loaded.length).toBe(0)})模拟window.location在业务代码中,经常需要使用当前的URL来判断场景,但是实际上我们不能直接给window.location赋值来模拟这种场景,怎么办呢?比如我们需要根据location.search中包含的具体参数值执行不同的代码分支。可以分两步模拟:自定义window对象,覆盖jsdom中的window对象,通过defineProperty给window.location对象赋值global.window=Object.create(window);consturl="http://dummy.com?foo=bar";Object.defineProperty(window,'location',{value:{href:url,search:'?foo=bar'}});expect(global.location.search.indexOf('foo')>-1).toBeTruthy()注意:由于覆盖了全局变量location,需要注意的是它只在某个测试用例中起作用,否则会导致其他测试用例失败。