美文网首页
[译] 对 Vue-Router 进行单元测试

[译] 对 Vue-Router 进行单元测试

作者: 880d91446f17 | 来源:发表于2018-11-06 21:34 被阅读33次

    由于路由通常会把多个组件牵扯到一起操作,所以一般对其的测试都在 端到端/集成 阶段进行,处于测试金字塔的上层。不过,做一些路由的单元测试还是大有益处的。

    对于与路由交互的组件,有两种测试方式:

    使用一个真正的 router 实例
    mock 掉 route 和router 全局对象
    因为大多数 Vue 应用用的都是官方的 Vue Router,所以本文会谈谈这个。

    创建组件

    我们会弄一个简单的 <App> ,包含一个 /nested-child 路由。访问 /nested-child 则渲染一个 <NestedRoute> 组件。创建 App.vue 文件,并定义如下的最小化组件:

    <template>
     <div id="app">
     <router-view />
     </div>
    </template>
    <script>
    export default {
     name: 'app'
    }
    </script>
    <NestedRoute> 同样迷你:
    
    <template>
     <div>Nested Route</div>
    </template>
    <script>
    export default {
     name: "NestedRoute"
    }```
    </script>
    现在定义一个路由:
    import NestedRoute from "@/components/NestedRoute.vue"
    export default [
     { path: "/nested-route", component: NestedRoute }
    ]```
    
    在真实的应用中,一般会创建一个 router.js 文件并导入定义好的路由,写出来一般是这样的:
    
    import Vue from "vue"
    import VueRouter from "vue-router"
    import routes from "./routes.js"
    Vue.use(VueRouter)
    export default new VueRouter({ routes })
    

    为避免调用 Vue.use(...) 污染测试的全局命名空间,我们将会在测试中创建基础的路由;这让我们能在单元测试期间更细粒度的控制应用的状态。

    编写测试

    先看点代码再说吧。我们来测试 App.vue ,所以相应的增加一个 App.spec.js :

    import { shallowMount, mount, createLocalVue } from "@vue/test-utils"
    import App from "@/App.vue"
    import VueRouter from "vue-router"
    import NestedRoute from "@/components/NestedRoute.vue"
    import routes from "@/routes.js"
    const localVue = createLocalVue()
    localVue.use(VueRouter)
    describe("App", () => {
     it("renders a child component via routing", () => {
     const router = new VueRouter({ routes })
     const wrapper = mount(App, { localVue, router })
     router.push("/nested-route")
     expect(wrapper.find(NestedRoute).exists()).toBe(true)
     })
    })
    

    照例,一开始先把各种模块引入我们的测试;尤其是引入了应用中所需的真实路由。这在某种程度上很理想 -- 若真实路由一旦挂了,单元测试就失败,这样我们就能在部署应用之前修复这类问题。

    可以在 <App> 测试中使用一个相同的 localVue ,并将其声明在第一个 describe 块之外。而由于要为不同的路由做不同的测试,所以把 router 定义在 it 块里。

    另一个要注意的是这里用了 mount 而非 shallowMount 。如果用了 shallowMount ,则 <router-link> 就会被忽略,不管当前路由是什么,渲染的其实都是一个无用的替身组件。

    为使用了 mount 的大型渲染树做些变通

    使用 mount 在某些情况下很好,但有时却是不理想的。比如,当渲染整个 <App> 组件时,正赶上渲染树很大,包含了许多组件,一层层的组件又有自己的子组件。这么些个子组件都要触发各种生命周期钩子、发起 API 请求什么的。

    如果你在用 Jest,其强大的 mock 系统为此提供了一个优雅的解决方法。可以简单的 mock 掉子组件,在本例中也就是 <NestedRoute> 。使用了下面的写法后,以上测试也将能通过:

    jest.mock("@/components/NestedRoute.vue", () => ({
     name: "NestedRoute",
     render: h => h("div")
    }))
    

    使用 Mock Router

    有时真实路由也不是必要的。现在升级一下 <NestedRoute> ,让其根据当前 URL 的查询字符串显示一个用户名。这次我们用 TDD 实现这个特性。以下是一个基础测试,简单的渲染了组件并写了一句断言:

    import { shallowMount } from "@vue/test-utils"
    import NestedRoute from "@/components/NestedRoute.vue"
    import routes from "@/routes.js"
    describe("NestedRoute", () => {
     it("renders a username from query string", () => {
     const username = "alice"
     const wrapper = shallowMount(NestedRoute)
     expect(wrapper.find(".username").text()).toBe(username)
     })
    })
    

    然而我们并没有 <div class="username"> ,所以一运行测试就会报错:

    tests/unit/NestedRoute.spec.js
     NestedRoute
     ✕ renders a username from query string (25ms)
     ● NestedRoute › renders a username from query string
     [vue-test-utils]: find did not return .username, cannot call text() on empty Wrapper
    来更新一下 <NestedRoute> :
    
    <template>
     <div>
     Nested Route
     <div class="username">
     {{ $route.params.username }}
     </div>
     </div>
    </template>
    

    现在报错变为了:

    tests/unit/NestedRoute.spec.js
     NestedRoute
     ✕ renders a username from query string (17ms)
     ● NestedRoute › renders a username from query string
     TypeError: Cannot read property 'params' of undefined
    

    这是因为 $route 并不存在。 我们当然可以用一个真正的路由,但在这样的情况下只用一个 mocks 加载选项会更容易些:

    it("renders a username from query string", () => {
     const username = "alice"
     const wrapper = shallowMount(NestedRoute, {
     mocks: {
     $route: {
     params: { username }
     }
     }
     })
     expect(wrapper.find(".username").text()).toBe(username)
    })
    

    这样测试就能通过了。在本例中,我们没有做任何的导航或是和路由的实现相关的任何其他东西,所以 mocks 就挺好。我们并不真的关心 username 是从查询字符串中怎么来的,只要它出现就好。

    测试路由钩子的策略

    Vue Router 提供了多种类型的路由钩子, 称为 “navigation guards”。举两个例子如:

    全局 guards ( router.beforeEach )。在 router 实例上声明
    组件内 guards,比如 beforeRouteEnter 。在组件中声明
    要确保这些运作正常,一般是集成测试的工作,因为需要一个使用者从一个理由导航到另一个。但也可以用单元测试检验导航 guards 中调用的函数是否正常工作,并更快的获得潜在错误的反馈。这里列出一些如何从导航 guards 中解耦逻辑的策略,以及为此编写的单元测试。

    全局 guards

    比方说当路由中包含 shouldBustCache 元数据的情况下,有那么一个 bustCache 函数就应该被调用。路由可能长这样:

    //routes.js
    import NestedRoute from "@/components/NestedRoute.vue"
    export default [
     {
     path: "/nested-route",
     component: NestedRoute,
     meta: {
     shouldBustCache: true
     }
     }
    ]
    

    之所以使用 shouldBustCache 元数据,是为了让缓存无效,从而确保用户不会取得旧数据。一种可能的实现如下:

    //router.js
    import Vue from "vue"
    import VueRouter from "vue-router"
    import routes from "./routes.js"
    import { bustCache } from "./bust-cache.js"
    Vue.use(VueRouter)
    const router = new VueRouter({ routes })
    router.beforeEach((to, from, next) => {
     if (to.matched.some(record => record.meta.shouldBustCache)) {
     bustCache()
     }
     next()
    })
    export default router
    

    在单元测试中,你可能想导入 router 实例,并试图通过 router.beforeHooks0 的写法调用 beforeEach ;但这将抛出一个关于 next 的错误 -- 因为没法传入正确的参数。针对这个问题,一种策略是在将 beforeEach 导航钩子耦合到路由中之前,解耦并单独导出它。做法是这样的:

    //router.js
    export function beforeEach((to, from, next) {
     if (to.matched.some(record => record.meta.shouldBustCache)) {
     bustCache()
     }
     next()
    }
    router.beforeEach((to, from, next) => beforeEach(to, from, next))
    export default router
    

    再写测试就容易了,虽然写起来有点长:

    import { beforeEach } from "@/router.js"
    import mockModule from "@/bust-cache.js"
    jest.mock("@/bust-cache.js", () => ({ bustCache: jest.fn() }))
    describe("beforeEach", () => {
     afterEach(() => {
     mockModule.bustCache.mockClear()
     })
     it("busts the cache when going to /user", () => {
     const to = {
     matched: [{ meta: { shouldBustCache: true } }]
     }
     const next = jest.fn()
     beforeEach(to, undefined, next)
     expect(mockModule.bustCache).toHaveBeenCalled()
     expect(next).toHaveBeenCalled()
     })
     it("busts the cache when going to /user", () => {
     const to = {
     matched: [{ meta: { shouldBustCache: false } }]
     }
     const next = 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> 添加了一个 beforeRouteLeave hook:

    //NestedRoute.vue
    <script>
    import { bustCache } from "@/bust-cache.js"
    export default {
     name: "NestedRoute",
     beforeRouteLeave(to, from, next) {
     bustCache()
     next()
     }
    }
    

    </script>
    对在全局 guard 中的方法照猫画虎就可以测试它了:

    // ...
    import NestedRoute from "@/compoents/NestedRoute.vue"
    import mockModule from "@/bust-cache.js"
    jest.mock("@/bust-cache.js", () => ({ bustCache: jest.fn() }))
    it("calls bustCache and next when leaving the route", () => {
     const next = jest.fn()
     NestedRoute.beforeRouteLeave(undefined, undefined, next)
     expect(mockModule.bustCache).toHaveBeenCalled()
     expect(next).toHaveBeenCalled()
    })
    

    这样的单元测试行之有效,可以在开发过程中立即得到反馈;但由于路由和导航 hooks 常与各种组件互相影响以达到某些效果,也应该做一些集成测试以确保所有事情如预期般工作。

    总结

    本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。
    对web开发技术感兴趣的同学,欢迎加入Q群:582735936,不管你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天更新视频资料。
    最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    相关文章

      网友评论

          本文标题:[译] 对 Vue-Router 进行单元测试

          本文链接:https://www.haomeiwen.com/subject/lwjpxqtx.html