16

I've looked at other answers with this problem, and it seems to be caused by trying to import vue-router into the test. This however, is not the case for my problem. Here is my test code:

import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import ListDetails from '../components/homepage/ListDetails'
import EntityList from '../components/homepage/EntityList'
import BootstrapVue from 'bootstrap-vue'
import Vuex from 'vuex'
import faker from 'faker'

const localVue = createLocalVue()

localVue.use(Vuex)
localVue.use(BootstrapVue)

describe('ListDetails.vue', () => {
    it('gets the correct page list when router list id param present', () => {
        const selected_list = {
            id: faker.random.number(),
            name: faker.lorem.words(),
            entries: []
        }

        testRouteListIdParam(selected_list)
    })
})

Then in testRouteListIdParam, I have:

function testRouteListIdParam(selected_list) {
    // just assume this sets up a mocked store properly.
    const store = setUpStore(selected_list, true)

    const $route = {
        path: `/homepage/lists/${selected_list.id}`
    }

    const wrapper = mount(ListDetails, {
        mocks: {
            $route
        },
        store,
        localVue
    })
}

As soon as mount() happens, I get the error:

[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property as a read-only value

Any ideas why this would be happening? Again, I'm not using VueRouter anywhere in the unit tests, so I'm not sure why I'm getting the error. Could it be BootstrapVue or Vuex that are messing things up?

tony19
  • 125,647
  • 18
  • 229
  • 307
janedoe
  • 907
  • 1
  • 13
  • 33

5 Answers5

19

So this is a bug with vue-test-utils. If you are using VueRouter anywhere (even if it's not used in any unit test), you will get the above error.

A work around is to use process.env.NODE_ENV in your unit tests and set it to 'test', and wherever you're using VueRouter, check process.env.NODE_ENV like so:

if (!process || process.env.NODE_ENV !== 'test') {
    Vue.use(VueRouter)
}

at least until vue-test-utils bug is fixed, this should fix this problem

janedoe
  • 907
  • 1
  • 13
  • 33
  • I don't think this is true, and I'm not able to reproduce it with a Vue CLI project. I was only able to reproduce the bug when installing VueRouter from a component's code path, which brings it into the unit test. Can you provide a link to a GitHub repo that reproduces the problem? I might be able to pinpoint it for you. – tony19 Apr 05 '19 at 22:58
  • yeah, i have it reproducible at this codesandbox: https://codesandbox.io/s/oxvvn2r96 if you run the one test suite there it will pass since $route is undefined, and console will output that $route cannot be modified as it's read only – janedoe Apr 05 '19 at 22:58
  • The error seems unique to that Codesandbox environment. See my [GitHub repo](https://github.com/tony19-sandbox/vue-router-mock-demo) that shows `$route` can be mocked without error. – tony19 Apr 05 '19 at 23:12
  • 3
    The issue has to do with importing the router file in any test. Here's an example of that shows the failure: https://github.com/blimmer/vue-router-mock-demo/commit/ed1246db9121a70c6ae0d4c220286b55ae800e4c . For us, we import the router in order to test global navigation guards. – blimmer Jun 06 '19 at 17:31
13

I think these docs are relevant to your situation:

Common gotchas

Installing Vue Router adds $route and $router as read-only properties on Vue prototype.

This means any future tests that try to mock $route or $router will fail.

To avoid this, never install Vue Router globally when you're running tests; use a localVue as detailed above.

The error you're seeing indicates that one of your components (and outside your test code) is installing VueRouter (e.g., Vue.use(VueRouter)).

To address the issue, search for the VueRouter installation in your component code path, including any of their imports, and refactor it so that the import is not required there. Typically, the installation of VueRouter exists only in main.js or its imports.

GitHub demo

tony19
  • 125,647
  • 18
  • 229
  • 307
1

I encountered this, and it was because I was importing vueRouter into a controller, outside of a vueComponent, where this.$router wasn't defined.

import router from '@/router';
router.push('/foo')
1

janedoe's answer can work but it's often risky to modify production code just to make some tests pass. I prefer to bypass the bug by doing this:

  1. Run your test in watch mode

    npx jest --watch src/components/Foo.test.ts
    
  2. Locate Vue.use(VueRouter) in your code and diagnose what is the chain of components indirectly running the code by adding this just above

    const error = new Error();
    console.log(
      error.stack
        ?.split('\n')
        .filter((line) => line.includes('.vue'))
        .join('\n'),
    );
    

    This logs a list of file path like

    console.log
    at Object.<anonymous> (/path/to/components/Bar.vue:1:1)
    at Object.<anonymous> (/path/to/components/Foo.vue:1:1)
    
  3. Chose a component in this list and, in your test file, mock it

    jest.mock('/path/to/components/Bar.vue');
    
Nino Filiu
  • 16,660
  • 11
  • 54
  • 84
0

As I got this error with $bvModal from bootstrap-vue and came across this page, I will post my solution here which might be helpful for anyone getting this error for read-only properties.

First of all, the reason as it is mentioned in other answers is that you register a component or plugin which has a read-only property, therefore you cannot mock it anymore. So the problem is only mocking it not the code itself and I'd rather not change the code.

I removed it from mocks and mocked it after mounting.

in my case we had this:

mockOptions = {
  propsData: mockProps,
  mocks: {
    $store: store,
    $route,
    $bvModal: {
      show: jest.fn(),
      hide: jest.fn(),
    },
  },
  localVue,
};

const wrapper = mount(ActionModals, mockOptions);         
expect(mockOptions.mocks.$bvModal.hide).toHaveBeenCalledWith('editItemModal');

I changed it to this:

mockOptions = {
  propsData: mockProps,
  mocks: {
    $store: store,
    $route,
  },
  localVue,
};

const wrapper = mount(ActionModals, mockOptions);  
jest.spyOn(wrapper.vm.$bvModal, 'hide').mockImplementation(jest.fn());       
expect(wrapper.vm.$bvModal.hide).toHaveBeenCalledWith('editItemModal');

Therefore for the route, if you have to register it, mock it after mounting.

rebinnaf
  • 276
  • 2
  • 9