Vue-router: Make $router and $route writable to allow stubbing in tests

Created on 27 Sep 2017  Β·  10Comments  Β·  Source: vuejs/vue-router

What problem does this feature solve?

One of the most common issue for users testing Vue components is that they can't stub $router or $route in a test case.

see vue-test-utils intercept option

This is usually caused by them requiring a file somewhere in their test suite that installs Vue Router.

Making $router and $route writable would stop users having this issue.

What does the proposed API look like?

We could set $router and $route as writable properties.

We could add a setter that logs a warning when the props are rewritten.

improvement

Most helpful comment

I take a look on the subject and I came to that reflexion :

I think it is a good practice to not have write access on $router and $route just for test cases. Indeed, it involves changes in production code just for test case(s) and it is commonly known as a bad practice.

I think that it should be the responsability of the test library to handle $router and $route mock.

Ok, now how could we do this ?

I found some solution to mock $route in my unit tests and here is how I did it :

import { createLocalVue, shallow } from 'vue-test-utils'
import { expect } from 'chai'

import SomeComponent from 'path/to/some-component'

describe('Some component...', () => {
  let vm, mockedRoute

  // before or beforeEach, do as you want
  before(() => {
    mockedRoute = { query: {} }
    const localVue = createLocalVue()
    vm = shallow(SomeComponent, {
      localVue,
      beforeCreate: function () {
        this._route = mockedRoute
      }
    }).vm
  })
  it('Should have "$route" defined and matching expected mock.', () => {
    expect(vm.$route).to.equals(mockedRoute)
  })
})

This follow the existing behavior of how vue-router inject $router and $route into a Vue instance.

Now I believe that it should be possible to implement this kind of solution into _vue-test-utils_.

Here is my reflexion :

import { createLocalVue, shallow } from 'vue-test-utils'
import { expect } from 'chai'

import SomeComponent from 'path/to/some-component'

describe('Some component...', () => {
  let vm, mockedRoute

  // before or beforeEach, do as you want
  before(() => {
    mockedRoute = { query: {} }
    const localVue = createLocalVue()
    vm = shallow(SomeComponent, {
      localVue,
      mocks: {
        $route: mockedRoute
      }
    }).vm
  })
  it('Should have "$route" defined and matching expected mock.', () => {
    expect(vm.$route).to.equals(mockedRoute)
  })
})

We could update the _vue-test-utils_ implementation to bind the _"mocks"_ part of the configuration options object to the _"beforeCreate"_ function of the Vue instance and do the "private" attributes initialization with provided mocks (like I did with _route and my $route mock).

What do you think about that ?

In any case, I strongly believe that you should not change _vue-router_ code in order to comply with some unit tests use cases (in this case at least).
I think that readonly access to $router and $route is a good thing that can prevent some unwanted developers mistakes during an application life and runtime as it seems to be an important Vue architecture design decision.

I am curious to have your points of view guys. Have a nice day.

All 10 comments

Adding the warning may be problematic unless it's behind a flag that is always turned off by tests but I yeah, we can do something to make it easily _stubbable_ πŸ™‚

Ok, I can make a PR this evening πŸ™‚

Hi here. Is there any news on the subject ?

Indeed, I am building an app and I am stuck with $route mocking for my unit tests.

Is there a working solution to implement during the issue resolution ?

Hey, so the PR was more complicated than I thought and I haven't got back round to it.

You can get around this by not importing any files that call Vue.use(Router) in your tests.

I take a look on the subject and I came to that reflexion :

I think it is a good practice to not have write access on $router and $route just for test cases. Indeed, it involves changes in production code just for test case(s) and it is commonly known as a bad practice.

I think that it should be the responsability of the test library to handle $router and $route mock.

Ok, now how could we do this ?

I found some solution to mock $route in my unit tests and here is how I did it :

import { createLocalVue, shallow } from 'vue-test-utils'
import { expect } from 'chai'

import SomeComponent from 'path/to/some-component'

describe('Some component...', () => {
  let vm, mockedRoute

  // before or beforeEach, do as you want
  before(() => {
    mockedRoute = { query: {} }
    const localVue = createLocalVue()
    vm = shallow(SomeComponent, {
      localVue,
      beforeCreate: function () {
        this._route = mockedRoute
      }
    }).vm
  })
  it('Should have "$route" defined and matching expected mock.', () => {
    expect(vm.$route).to.equals(mockedRoute)
  })
})

This follow the existing behavior of how vue-router inject $router and $route into a Vue instance.

Now I believe that it should be possible to implement this kind of solution into _vue-test-utils_.

Here is my reflexion :

import { createLocalVue, shallow } from 'vue-test-utils'
import { expect } from 'chai'

import SomeComponent from 'path/to/some-component'

describe('Some component...', () => {
  let vm, mockedRoute

  // before or beforeEach, do as you want
  before(() => {
    mockedRoute = { query: {} }
    const localVue = createLocalVue()
    vm = shallow(SomeComponent, {
      localVue,
      mocks: {
        $route: mockedRoute
      }
    }).vm
  })
  it('Should have "$route" defined and matching expected mock.', () => {
    expect(vm.$route).to.equals(mockedRoute)
  })
})

We could update the _vue-test-utils_ implementation to bind the _"mocks"_ part of the configuration options object to the _"beforeCreate"_ function of the Vue instance and do the "private" attributes initialization with provided mocks (like I did with _route and my $route mock).

What do you think about that ?

In any case, I strongly believe that you should not change _vue-router_ code in order to comply with some unit tests use cases (in this case at least).
I think that readonly access to $router and $route is a good thing that can prevent some unwanted developers mistakes during an application life and runtime as it seems to be an important Vue architecture design decision.

I am curious to have your points of view guys. Have a nice day.

Thanks for your suggestion πŸ™‚.

In fact, you can safely install VueRouter on a localVue in tests. You can see details on it in the guideβ€”https://vue-test-utils.vuejs.org/en/guides/using-with-vue-router.html

I agree that changing production code for tests is bad practice. But a lot of people get stung by $router and $route being added as read only propteries. They might import a file in their test that imports a file that calls Vue.use(VueRouter). This is difficult to debug.

I don't have all the history of $router and $route as read only properties and as I think I understand it looks like a breaking change comparing to some oldest behavior.

The fact is that for now, none of every proposed solutions to mock $route seems to work (I think I tried it all) (There is some kind of workaround, the one I posted previously 😏 ).

Mocking $router is easy because we can provide a fake object to _router_ mount options object property.

When I look to _vue-test-utils_ implementation to inject mocks to Vue instance, it looks something like this :

// add-mocks.js

export default function addMocks (mockedProperties: Object, Vue: Component) {
  Object.keys(mockedProperties).forEach((key) => {
    Vue.prototype[key] = mockedProperties[key]
  })
}

So we only have 2 solutions here :

  • Change _vue-router_ to allow write access to $route (at least) and $router so that _vue-test-utils_ current implementation can continue to inject mocks the way it does (Personally I think that it looks a little "brutal" πŸ˜‰ ).
  • Change _vue-test-utils_ to inject mocks in the Vue's way and keep _vue-router_ (and maybe more ?) have a clean and utopian implementation for $router and $route πŸ˜‡ .

After a lot of readings, doing some Vue.use(...) is a bad practice. If you are forced to do it, you have to do it at least on some "localVue".

I think that in fact, implementing one or another solution will not change the way we write tests for our Vue components.
Indeed, it will continue to looks like

shallow(SomeComponent, {
  localVue,
  mocks: {
    $route
  }
})

I don't claim to know everything on the subject but as far as I know, it will looks easy to debug in the end (not for now, which leads us to this issue and discussion).

I will enjoy if I missed some point and you can light me.
If there is something I can do to help to fix this the best way we can find it will be my pleasure to contribute.

In the end, thank you for your answers and for doing your best to find a solution.

Regards.

@eddyerburgh is this necessary anymore? We can stub $router and $route with vtu now

It would be less confusing for some users if they were writable. But there are lots of resources warning users not to instal Vue Router on the global constructor, so it's not vital.

Closing as we will very likely support a mocked version of vue-router for tests in the future and the scope of it is way bigger than just writing over these properties and needs to go through the RFC process

Was this page helpful?
0 / 5 - 0 ratings