对 Vue-Router 进行单元测试的方法
由于路由通常会把多个组件牵扯到一起操作,所以一般对其的测试都在端到端/集成阶段进行,处于测试金字塔的上层。不过,做一些路由的单元测试还是大有益处的。
对于与路由交互的组件,有两种测试方式:
- 使用一个真正的router实例
- mock掉$route 和$router 全局对象
因为大多数Vue应用用的都是官方的VueRouter,所以本文会谈谈这个。
创建组件
我们会弄一个简单的
NestedRoute
现在定义一个路由:
importNestedRoutefrom"@/components/NestedRoute.vue" exportdefault[ {path:"/nested-route",component:NestedRoute} ]
在真实的应用中,一般会创建一个router.js 文件并导入定义好的路由,写出来一般是这样的:
importVuefrom"vue" importVueRouterfrom"vue-router" importroutesfrom"./routes.js" Vue.use(VueRouter) exportdefaultnewVueRouter({routes})
为避免调用Vue.use(...) 污染测试的全局命名空间,我们将会在测试中创建基础的路由;这让我们能在单元测试期间更细粒度的控制应用的状态。
编写测试
先看点代码再说吧。我们来测试App.vue,所以相应的增加一个 App.spec.js:
import{shallowMount,mount,createLocalVue}from"@vue/test-utils" importAppfrom"@/App.vue" importVueRouterfrom"vue-router" importNestedRoutefrom"@/components/NestedRoute.vue" importroutesfrom"@/routes.js" constlocalVue=createLocalVue() localVue.use(VueRouter) describe("App",()=>{ it("rendersachildcomponentviarouting",()=>{ constrouter=newVueRouter({routes}) constwrapper=mount(App,{localVue,router}) router.push("/nested-route") expect(wrapper.find(NestedRoute).exists()).toBe(true) }) })
照例,一开始先把各种模块引入我们的测试;尤其是引入了应用中所需的真实路由。这在某种程度上很理想--若真实路由一旦挂了,单元测试就失败,这样我们就能在部署应用之前修复这类问题。
可以在
另一个要注意的是这里用了mount 而非shallowMount。如果用了 shallowMount,则
为使用了mount的大型渲染树做些变通
使用mount 在某些情况下很好,但有时却是不理想的。比如,当渲染整个
如果你在用Jest,其强大的mock系统为此提供了一个优雅的解决方法。可以简单的mock掉子组件,在本例中也就是
jest.mock("@/components/NestedRoute.vue",()=>({ name:"NestedRoute", render:h=>h("div") }))
使用MockRouter
有时真实路由也不是必要的。现在升级一下
import{shallowMount}from"@vue/test-utils" importNestedRoutefrom"@/components/NestedRoute.vue" importroutesfrom"@/routes.js" describe("NestedRoute",()=>{ it("rendersausernamefromquerystring",()=>{ constusername="alice" constwrapper=shallowMount(NestedRoute) expect(wrapper.find(".username").text()).toBe(username) }) })
然而我们并没有
tests/unit/NestedRoute.spec.js
NestedRoute
✕rendersausernamefromquerystring(25ms)●NestedRoute›rendersausernamefromquerystring
[vue-test-utils]:finddidnotreturn.username,cannotcalltext()onemptyWrapper
来更新一下
NestedRoute{{$route.params.username}}
现在报错变为了:
tests/unit/NestedRoute.spec.js
NestedRoute
✕rendersausernamefromquerystring(17ms)●NestedRoute›rendersausernamefromquerystring
TypeError:Cannotreadproperty'params'ofundefined
这是因为$route 并不存在。我们当然可以用一个真正的路由,但在这样的情况下只用一个mocks 加载选项会更容易些:
it("rendersausernamefromquerystring",()=>{ constusername="alice" constwrapper=shallowMount(NestedRoute,{ mocks:{ $route:{ params:{username} } } }) expect(wrapper.find(".username").text()).toBe(username) })
这样测试就能通过了。在本例中,我们没有做任何的导航或是和路由的实现相关的任何其他东西,所以mocks 就挺好。我们并不真的关心username 是从查询字符串中怎么来的,只要它出现就好。
测试路由钩子的策略
VueRouter提供了多种类型的路由钩子,称为“navigationguards”。举两个例子如:
- 全局guards(router.beforeEach)。在router实例上声明
- 组件内guards,比如beforeRouteEnter。在组件中声明
要确保这些运作正常,一般是集成测试的工作,因为需要一个使用者从一个理由导航到另一个。但也可以用单元测试检验导航guards中调用的函数是否正常工作,并更快的获得潜在错误的反馈。这里列出一些如何从导航guards中解耦逻辑的策略,以及为此编写的单元测试。
全局guards
比方说当路由中包含shouldBustCache 元数据的情况下,有那么一个bustCache 函数就应该被调用。路由可能长这样:
//routes.js importNestedRoutefrom"@/components/NestedRoute.vue" exportdefault[ { path:"/nested-route", component:NestedRoute, meta:{ shouldBustCache:true } } ]
之所以使用shouldBustCache 元数据,是为了让缓存无效,从而确保用户不会取得旧数据。一种可能的实现如下:
//router.js importVuefrom"vue" importVueRouterfrom"vue-router" importroutesfrom"./routes.js" import{bustCache}from"./bust-cache.js" Vue.use(VueRouter) constrouter=newVueRouter({routes}) router.beforeEach((to,from,next)=>{ if(to.matched.some(record=>record.meta.shouldBustCache)){ bustCache() } next() }) exportdefaultrouter
在单元测试中,你可能想导入router实例,并试图通过router.beforeHooks[0]() 的写法调用beforeEach;但这将抛出一个关于 next 的错误--因为没法传入正确的参数。针对这个问题,一种策略是在将beforeEach 导航钩子耦合到路由中之前,解耦并单独导出它。做法是这样的:
//router.js exportfunctionbeforeEach((to,from,next){ if(to.matched.some(record=>record.meta.shouldBustCache)){ bustCache() } next() } router.beforeEach((to,from,next)=>beforeEach(to,from,next)) exportdefaultrouter
再写测试就容易了,虽然写起来有点长:
import{beforeEach}from"@/router.js" importmockModulefrom"@/bust-cache.js" jest.mock("@/bust-cache.js",()=>({bustCache:jest.fn()})) describe("beforeEach",()=>{ afterEach(()=>{ mockModule.bustCache.mockClear() }) it("buststhecachewhengoingto/user",()=>{ constto={ matched:[{meta:{shouldBustCache:true}}] } constnext=jest.fn() beforeEach(to,undefined,next) expect(mockModule.bustCache).toHaveBeenCalled() expect(next).toHaveBeenCalled() }) it("buststhecachewhengoingto/user",()=>{ constto={ matched:[{meta:{shouldBustCache:false}}] } constnext=jest.fn() beforeEach(to,undefined,next) expect(mockModule.bustCache).not.toHaveBeenCalled() expect(next).toHaveBeenCalled() }) })
最主要的有趣之处在于,我们借助jest.mock,mock掉了整个模块,并用afterEach 钩子将其复原。通过将beforeEach 导出为一个已结耦的、普通的Javascript函数,从而让其在测试中不成问题。
为了确定hook真的调用了bustCache 并且显示了最新的数据,可以使用一个诸如Cypress.io的端到端测试工具,它也在应用脚手架vue-cli 的选项中提供了。
组件guards
一旦将组件guards视为已结耦的、普通的Javascript函数,则它们也是易于测试的。假设我们为
//NestedRoute.vue
对在全局guard中的方法照猫画虎就可以测试它了:
//... importNestedRoutefrom"@/compoents/NestedRoute.vue" importmockModulefrom"@/bust-cache.js" jest.mock("@/bust-cache.js",()=>({bustCache:jest.fn()})) it("callsbustCacheandnextwhenleavingtheroute",()=>{ constnext=jest.fn() NestedRoute.beforeRouteLeave(undefined,undefined,next) expect(mockModule.bustCache).toHaveBeenCalled() expect(next).toHaveBeenCalled() })
这样的单元测试行之有效,可以在开发过程中立即得到反馈;但由于路由和导航hooks常与各种组件互相影响以达到某些效果,也应该做一些集成测试以确保所有事情如预期般工作。
总结
本文讲述了:
- 测试由VueRouter条件渲染的组件
- 用jest.mock 和localVue 去mockVue组件
- 从router中解耦全局导航guard并对其独立测试
- 用jest.mock 来mock一个模块
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。