【问题标题】:How to write test that mocks the $route object in vue components如何编写模拟 vue 组件中的 $route 对象的测试
【发布时间】:2017-05-18 10:39:21
【问题描述】:

我有一个包含 this.$route.fullPath 之类的语句的组件,如果我想测试该组件,我应该如何模拟 fullPathof $route 对象的值?

【问题讨论】:

    标签: unit-testing sinon vue-component vuejs2 vue-loader


    【解决方案1】:

    我不同意最佳答案 - 您可以毫无问题地模拟 $route

    另一方面,在基础构造函数上多次安装 vue-router 给你带来问题。它将$route$router 添加为只读属性。这使得在以后的测试中无法覆盖它们。

    vue-test-utils 有两种方法可以实现这一目标。

    mocks option模拟vue-router

    const $route = {
        fullPath: 'full/path'
    }
    const wrapper = mount(ComponentWithRouter, { 
      mocks: {
        $route
      } 
    })
    
    wrapper.vm.$route.fullPath // 'full/path'
    

    您还可以使用 createLocalVue 安全地安装 Vue Router:

    在使用createLocalVue的测试中安全安装 vue-router

    const localVue = createLocalVue()
    localVue.use(VueRouter)
    const routes = [
     {
       path: '/',
       component: Component
     }
    ]
    const router = new VueRouter({
     routes
    })
    const wrapper = mount(ComponentWithRouter, { localVue, router })
    expect(wrapper.vm.$route).to.be.an('object')
    

    【讨论】:

    • 第一种方法不起作用:TypeError: Cannot set property $route of #<Vue$3> which has only a getter.
    • 您好 Daniel,由于您在测试期间已经安装了 VueRouter,因此引发了此错误。调用 Vue.use(VueRouter) 后,$route 和 $router 将被添加为只读属性。为了能够在测试中修改 $route 或 $router 属性,您不能在全局基础构造函数上安装 Vue Router。
    • 如何使用第一种方法模拟 $store?
    • 值得注意的是,vue-cli 在 router/index.js 中删除了一个 Vue.use(Router),它被加载到全局 Vue 中。如果您说,在您的组件中调用 router.push,它会在测试之前导入该全局只读 $router(因此无法模拟)。
    • @MatthewWhite 您可以使用 push 方法模拟 $route 对象,它会正常工作!
    【解决方案2】:

    最好不要模拟vue-router,而是用它来渲染组件,这样你就可以得到一个正常工作的路由器。示例:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import totest from 'src/components/totest'
    
    describe('totest.vue', () => {
      it('should totest renders stuff', done => {
        Vue.use(VueRouter)
        const router = new VueRouter({routes: [
            {path: '/totest/:id', name: 'totest', component: totest},
            {path: '/wherever', name: 'another_component', component: {render: h => '-'}},
        ]})
        const vm = new Vue({
          el: document.createElement('div'),
          router: router,
          render: h => h('router-view')
        })
        router.push({name: 'totest', params: {id: 123}})
        Vue.nextTick(() => {
          console.log('html:', vm.$el)
          expect(vm.$el.querySelector('h2').textContent).to.equal('Fred Bloggs')
          done()
        })
      })
    })
    

    注意事项:

    1. 我使用的是仅运行时版本的 vue,因此是 render: h => h('router-view')
    2. 我只测试totest 组件,但如果totest 引用了其他组件,则可能需要其他组件,例如。 another_component 在这个例子中。
    3. 您需要 nextTick 才能呈现 HTML,然后才能查看/测试它。

    其中一个问题是我发现的大多数示例都引用了旧版本的vue-router,参见the migrations docs,例如。一些示例使用 router.go() 现在不起作用。

    【讨论】:

    • 这个答案为我节省了很多时间。但是它可以使用一些改进:如果您有多个测试。如果你想重置状态,你实际上必须路由到 /whatever 然后到 /totest 。最好找到一种方法来销毁路由器并重新构建它。当我在每次测试中构建我的路由器时,我都遇到了奇怪的行为(似乎路由器状态是持久的,并且路由到同一个“页面”并没有触发状态更改)
    • here 是一个正在运行的完整测试设置。这可能是描述完整测试设置的最清晰的方法。
    • @SColvin 链接已失效:/(您评论中的那个)
    • here 是 2 月份的测试文件。测试现在更加完整,但可能不太容易开始。
    • 我不同意你不应该模拟 Vue 路由器。看我的回答——stackoverflow.com/a/44619365/4939630
    【解决方案3】:

    没有答案可以帮助我,所以我深入研究了 vue-test-utils 文档,发现自己是一个有效的答案,所以你需要导入。

    import { shallowMount,createLocalVue } from '@vue/test-utils';
    import router from '@/router.ts';
    const localVue = createLocalVue();
    

    我们创建了一个示例 vue 实例。测试时您需要使用shallowMount,以便您可以提供vue 应用实例和路由器。

    describe('Components', () => {
      it('renders a comment form', () => {
        const COMMENTFORM = shallowMount(CommentForm,{
          localVue,
          router
        });
      })
    })
    

    您可以轻松地通过路由器并进行浅层安装,并且不会出现错误。如果你想通过商店你使用:

    import { shallowMount,createLocalVue } from '@vue/test-utils';
    import router from '@/router.ts';
    import store from '@/store.ts';
    const localVue = createLocalVue();
    

    然后通过store:

    describe('Components', () => {
      it('renders a comment form', () => {
        const COMMENTFORM = shallowMount(CommentForm,{
          localVue,
          router,
          store
        });
      })
    })
    

    此解决方案解决了以下错误:

    • 使用this.$route.params.id 时无法读取未定义的属性“参数”
    • 未知的自定义元素router-link

    【讨论】:

    • 这个解决方案实际上并没有在任何地方模拟 $route。
    • @mcv 你有解决方案吗?
    【解决方案4】:

    我发现最简单的方法是使用 localVue

    import { createLocalVue, mount } from '@vue/test-utils';
    import VueRouter from 'vue-router';
    import Vuex from 'vuex';
    
    import ComponentName from '@/components/ComponentName.vue';
    // Add store file if any getters is accessed
    import store from '@/store/store';
    
    describe('File name', () => {
      const localVue = createLocalVue();
      localVue.use(VueRouter);
    
      // Can also be replaced with route(router.js) file
      const routes = [
        {
          path: '/path',
          component: ComponentName,
          name: 'Route name'
        }
      ];
    
      const router = new VueRouter({ routes });
    
      // if needed
      router.push({
        name: 'Route name',
        params: {}
      });
    
      const wrapper = mount(ComponentName, {
        localVue,
        router,
        store
      });
    
      test('Method()', () => {
        wrapper.vm.methodName();
    
        expect(wrapper.vm.$route.path)
          .toEqual(routes[0].path);
      });
    });
    

    希望对你有帮助!!!

    【讨论】:

      【解决方案5】:

      您不必专门“模拟”路由器。您的应用程序可以在全局 vue 范围内设置 VueRouter,并且您仍然可以让它在您的测试中做您想做的事情而不会出现问题。

      使用VueRouterhttps://vue-test-utils.vuejs.org/guides/#using-with-vue-router 阅读localVue 使用情况。

      我目前正在从我们的主应用程序中提取一个复杂的路由器,并且能够jest.spyOn() 调用router.push() 以及在创建组件之前设置路径,运行shallowMount() 以在@987654328 中进行一些路由处理@钩子。

      解决方法

      // someVueComponent.vue

      <template>
      ... something
      </template>
      <script>
      ...
      data () {
        return {
          authenticated: false
        }
      },
      ...
      created () {
        if(!this.authenticated && this.$route.path !== '/'){
          this.$router.push('/')
        }
      }
      </script>
      

      // someVueComponent.spec.js

      import Vuex from 'vuex'
      import VueRouter from 'vue-router'
      import { shallowMount, createLocalVue } from '@vue/test-utils'
      import SomeVueComponent from 'MyApp/components/someVueComponent'
      import MyAppRouter from 'MyApp/router'
      import MyAppCreateStore from 'MyApp/createStore'
      import merge from 'lodash.merge'
      
      function setVueUseValues (localVue) {
        localVue.use(Vuex)
        localVue.use(VueRouter)
        // other things here like custom directives, etc
      }
      
      beforeEach(() => {
        // reset your localVue reference before each test if you need something reset like a custom directive, etc
        localVue = createLocalVue()
        setVueUseValues(localVue)
      })
      
      let localVue = createLocalVue()
      setVueUseValues(localVue)
      
      test('my app does not react to path because its default is "/"', () => {
        const options = {
          localVue,
          router: MyAppRouter,
          store: MyAppCreateStore()  
        }  
      
        const routerPushSpy = jest.spyOn(options.router, 'push')
        const wrapper = shallowMount(SomeVueComponent, options)
        expect(routerPushSpy).toHaveBeenCalledTimes(0)
      })
      
      test('my app reacts to path because its not "/" and were not authenticated', () => {
        const options = {
          localVue,
          router: MyAppRouter,
          store: MyAppCreateStore()  
        }
      
        const routerPushSpy = jest.spyOn(options.router, 'push')
        options.router.push('/nothomepath')
        expect(routerPushSpy).toHaveBeenCalledWith('/nothomepath') // <- SomeVueComponent created hook will have $route === '/nothomepath' as well as fullPath
      
        const wrapper = shallowMount(SomeVueComponent, options)
        expect(routerPushSpy).toHaveBeenCalledWith('/') // <- works
      })
      

      上面的想法是我需要在创建/安装SomeVueComponent.vue 之前更改$route 状态。假设您可以创建包装器并希望根据其他状态或操作测试组件 this.$router.push('/something'),您可以随时监视 wrapper.vm 实例

      let routerPushSpy = jest.spyOn(wrapper.vm.$router, 'push') // or before hooks, etc
      

      在撰写本文时,似乎存在一个未解决的缺陷,因为vm.$route 将始终未定义,所以上面的唯一选项(据我所知)没有其他方法可以“模拟” " $route 因为安装 VueRouter 会将只读属性写入$route

      来自 vue-test-utils 文档https://vue-test-utils.vuejs.org/guides/#mocking-route-and-router

      import { shallowMount } from '@vue/test-utils'
      
      const $route = {
        path: '/some/path'
      }
      
      const wrapper = shallowMount(Component, {
        mocks: {
          $route
        }
      })
      
      wrapper.vm.$route.path // /some/path
      

      如果您对这里感兴趣的是该问题的 github 链接:https://github.com/vuejs/vue-test-utils/issues/1136

      【讨论】:

        【解决方案6】:

        感谢@SColvin 的回答;在我的场景中帮助找到了答案,其中我有一个带有路由器链接的组件正在抛出

        ERROR: '[Vue warn]: Error in render function: (found in <RouterLink>)'
        

        在单元测试期间,因为没有为 Vue 提供路由器。使用@SColvin 的答案重写最初由 vue-cli 从

        提供的测试
        describe('Hello.vue', () =>
        {
          it('should render correct contents', () =>
          {
            const Constructor = Vue.extend(Hello);
            const vm = new Constructor().$mount();
            expect(vm.$el.querySelector('.hello h1').textContent)
              .to.equal('Welcome to Your Vue.js App');
          });
        

        describe('Hello.vue', () =>
        {
          it('should render correct contents', () =>
          {
            Vue.use(VueRouter);
            const router = new VueRouter({
              routes: [
                { path: '/', name: 'Hello', component: Hello },
              ],
            });
            const vm = new Vue({
              el: document.createElement('div'),
              /* eslint-disable object-shorthand */
              router: router,
              render: h => h('router-view'),
            });
            expect(vm.$el.querySelector('.hello h1').textContent)
              .to.equal('Welcome to Your Vue.js App');
          });
        });
        

        不需要向视图传递参数我可以将组件简化为默认渲染,无需推送,无需等待nextTick。 HTH 别人!

        【讨论】:

          【解决方案7】:

          除了来自@SColvin 的出色回答之外,这是一个使用Avoriaz 工作的示例:

          import { mount } from 'avoriaz'
          import Vue from 'vue'
          import VueRouter from 'vue-router'
          import router from '@/router'
          import HappyComponent from '@/components/HappyComponent'
          
          Vue.use(VueRouter)
          
          describe('HappyComponent.vue', () => {
            it('renders router links', () => {
              wrapper = mount(HappyComponent, {router})
              // Write your test
            })
          })
          

          我相信这也适用于 vue-test-utils

          【讨论】:

          • 路线前的@/是什么意思?
          • @webdevguy 这是一个常见的Webpack resolve alias,Vue 团队将其用作src 目录的快捷方式。
          【解决方案8】:

          看看这个使用 vue-test-utils 的例子,我在其中模拟了路由器和存储。

          import ArticleDetails from '@/components/ArticleDetails'
          import { mount } from 'vue-test-utils'
          import router from '@/router'
          
          describe('ArticleDetails.vue', () => {
            it('should display post details', () => {
              const POST_MESSAGE = 'Header of our content!'
          
              const EXAMPLE_POST = {
                title: 'Title',
                date: '6 May 2016',
                content: `# ${POST_MESSAGE}`
              }
          
              const wrapper = mount(ArticleDetails, {
                router,
          
                mocks: {
                  $store: {
                    getters: {
                      getPostById () {
                        return EXAMPLE_POST
                      }
                    }
                  }
                }
              })
          
              expect(wrapper.vm.$el.querySelector('h1.post-title').textContent.trim()).to.equal(EXAMPLE_POST.title)
              expect(wrapper.vm.$el.querySelector('time').textContent.trim()).to.equal(EXAMPLE_POST.date)
              expect(wrapper.vm.$el.querySelector('.post-content').innerHTML.trim()).to.equal(
                `<h1>${POST_MESSAGE}</h1>`
              )
            })
          })
          

          【讨论】:

            【解决方案9】:

            这就是我按照this article所做的:

            it('renders $router.name', () => {
                const scopedVue = Vue.extend();
            
                const mockRoute = {
                    name: 'abc'
                };
            
                scopedVue.prototype.$route = mockRoute;
            
                const Constructor = scopedVue.extend(Component);
                const vm = new Constructor().$mount();
                expect(vm.$el.textContent).to.equal('abc');
            });
            

            【讨论】:

              【解决方案10】:

              您可以通过设置 vm._routerRoot._router 模拟到 vm.$router

              例如

              var Constructor      = Vue.extend(Your_Component)
              var vm               = new Constructor().$mount()
              var your_mock_router = {hello:'there'}
              
              vm.$router             = your_mock_router //An error 'setting a property that has only a getter'
              vm._routerRoot._router = your_mock_router //Wow, it works!
              

              你可以在这里仔细检查他们的源代码:https://github.com/vuejs/vue-router/blob/dev/dist/vue-router.js#L558

              【讨论】:

                【解决方案11】:

                我发现最简单的方法是模拟 $route。

                it('renders $router.name', () => {
                  const $route = {
                    name: 'test name - avoriaz'
                  }
                
                
                 const wrapper = shallow(Component, {
                    mocks: {
                      $route
                    }
                  })
                  expect(wrapper.text()).to.equal($route.name)
                })
                

                【讨论】:

                  【解决方案12】:

                  为什么所有的答案都这么复杂?你可以这样做:

                  ...
                  wrapper = mount(HappyComponent, {
                    mocks: {
                      $route: {fullPath: ''}
                    },
                  })
                  ...
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2023-03-04
                    • 2018-08-09
                    • 2019-01-18
                    • 2010-10-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多