Foreword: This article uses vue-cli’s path conventions for import and require. @ in path is a alias for the src directory at the root of the Vue project.

Recently, our company started to use VueJS. I’m not completely unhappy to say that as I love VueJS so much. I find the framework so well-designed and so easy to use. And part of that easiness is the huge ecosystem of plugins that has developed around it over the years. There are so many useful extensions that it’s quite hard not to end up with an application init file that doesn’t pile dozens of Vue.use().

But there’s a gotcha with this. Eventually, it’s easy to produce code that is tighly coupled with a handful of these plugin. This is a real problem when you want to unit-test your components usingvue-test-utils. Its API features a fancy localVue option that lets you create an isolated instance of Vue that your component will use under test. This may be useful if you want to mock of spy some of Vue’s methods. For instance Vue.set. But, as said, if you use several plugins, you will quickly end up repeating several Vue.use() in every of your test file’s beforeAll.

So, as any decent developer, you may want to separate the repeating code in a function. For instance, in a file called application-init.js, where you put your… well, application initialization code:

import VueAxios from "vue-axios";
import Vuetify from "vuetify";
import colors from "vuetify/es5/util/colors";

import App from "@/App";
import router from "@/router";
import { api, auth as servicesAuth } from "@/services";
import store from "@/store";
import i18n from "@/i18n";

const defaultTheme = {
  primary: colors.blue
};

function applicationInit(VueInstance, { axiosInstance = api, auth = servicesAuth, theme = defaultTheme } = {} /* passing your default there */) {
  VueInstance.use(VueAxios, axiosInstance);
  axiosInstance.defaults.baseURL = store.state.applicationConfiguration.baseUrl;

  VueInstance.router = router;

  VueInstance.use(require("@websanova/vue-auth"), auth);

  VueInstance.use(Vuetify, { theme });

  VueInstance.config.productionTip = false;

  return VueInstance;
}

function getApplication(VueInstance) {
  const Application = new VueInstance({
    router,
    store,
    i18n: i18n(VueInstance),
    render: h => h(App)
  }).$mount("#app");

  return Application;
}

export { applicationInit, getApplication };

There. Better. Now, in your test, you can just put:

import { applicationInit } from "@/application-init";
import { createLocalVue } from "@vue/test-utils";

import i18n from "@/i18n";
import store from "@/store";
import router from "@/router";
import AwesomeComponent from "@/components/AwesomeComponent";

describe("AwesomeComponent", () => {
    test("Does something awesome", () => {
        const localVue = applicationInit(createLocalVue());
        const target = shallowMount(AwesomeComponent, {
            localVue,
            store,
            router,
            i18n: i18n(mockLocalVue)
        });
});

Ok, first problem solved but, as in any hectic advanture, you then are trapped into the next episode of your journey : some of the plugin you use add methods, components and directives directly into the global namespace.

For instance, Axios has become the de-facto standard for making HTTP requests with Vue. And one of the places you might want to make requests is your Vuex store. Since, as far as I know, Vuex doesn’t let you pass custom options in its constructor, one of the solution you get is to use something like vue-axios that adds your Axios instance to each of your components and to the global Vue object.

This makes unit testing more difficult because, if, for some reason, you have to use a local Vue instance for testing your component, the Vue instance that your component uses, and the Vue instance that your component’s store uses, are not the same. Hopefully enought, Jest lets you mock a module entirely, using jest.mock(). Usage seems pretty straightforward, so, let’s go for it. Let’s say, you have a user menu and you want to test the logout feature. You are using @websanova/vue-auth and you want to test that, when you click your component’s logout button, it calls vue-auth’s logout method. Since you need to perform some cleanup actions inside the store before and after the logout, let’s say you put all your logout code in your store. So you have an actions.js that contains your store’s actions, and, in particular, the logout action:

// @/store/actions.js
import Vue from "vue";

const actions = {
    // some actions here
    async logout({ dispatch }) {
        await Vue.auth.logout();
        return dispatch("resetSession"); // Perform some cleanup here
    }
    // some more actions here
}

export { actions };
// @/components/UserMenu.vue
<template>
    <button @click="logout">Logout</button>
</template>

<script>
export default {
  name: "UserMenu",
  methods: {
    async logout() {
        await this.$store.dispatch("logout")
        return this.$router.push({ name: routeNames.LOGIN });
    },
  }
};
</script>

Alright, let’s test now:

import { applicationInit } from "@/application-init";
import { createLocalVue } from "@vue/test-utils";

import i18n from "@/i18n";
import store from "@/store";
import router from "@/router";

const localVue = applicationInit(createLocalVue());
jest.mock("vue", () => localVue);

import AwesomeComponUserMenuent from "@/components/UserMenu";

describe("The VideoConference component", () => {
  test("logout", async function() {
    jest.spyOn(localVue.auth, "logout").mockImplementation(jest.noop);

    const target = shallowMount(UserMenu, {
      localVue: localVue,
      store,
      router,
      i18n: i18n(localVue)
    });

    await target.vm.logout();
    expect(localVue.auth.logout).toHaveBeenCalled();
  });
});

Ok, let’s run it:

Test suite failed to run

    ReferenceError: localVue is not defined

      1 | import VueAxios from "vue-axios";
        | ^
      3 | import Vuetify from "vuetify";
      4 | import colors from "vuetify/es5/util/colors";

Wait… What!? How dare you!? localVue is defined! Just above jest.mock("vue", () => localVue);!

Ok, I give you the answer. The explanation of the problem is actually buried deep in Jest’s documentation:

When using babel-jest, calls to mock will automatically be hoisted to the top of the code block.

But it’s written in the documentation of another function. The kind of thing I actually tend to miss when I discover a new framework…

So what Jest actually do is it will execute any jest.mock() instruction at the very first, before doing anything else. Including before resolving the imports and initializing localVue. It’s a bit tricky since the second argument of jest.mock(), the one that provides the mock object is a function. So, intuitively, you may think that Jest will start mocking the module, only when it reaches the instruction. And the documentation is not very clear about that since this behaviour is explained in another section.

So, what you actually want to use is jest.doMock() which has the same signature and does the same thing except it does not hoist to the top of the code block. So it starts mocking when it is exectuted. Which is what we actually want. So let’s change jest.mock() by jest.doMock() and execute the test again aaaaaaand:

TypeError: Cannot read property 'logout' of undefined

Wait… What!?

Here, I stumbled upon an hour or so to get what was happennig. Vue.auth is undefined. It should not be, though, since we add the vue-auth to the local Vue instance in our init script and since now the Vue module is mocked, everything coming after jest.doMock should import the local Vue instance rather that the dependency. But it’s not the case. Actually, imports are actually resolved before executing any other code (which, as a second though, makes sense).

To solve this, you have two solutions:

using ES2015’s import() function which dynamically imports module or using require which is always dynamic. A problem with import() function is that it returns a promise. And promises in JS are much like black holes: when you step into one, you can’t escape and you code has to use promises as long as you need to work with the result of the promise. This is because dynamically imported modules are fetched from the server at runtime which could explode at the user’s face if the network breaks down.

It’s not ideal, it bloats the code and, usually, I’m not a fan of changing the architecture of application code to make it more testable. If developpers think an architecture is the best to answer a problem, tests should adapt to it. Not the other way around. This is why I’m not a fan of the argument “singleton are bad because they are hard to test”. But that’s another story. Here, the change is small and, used wisely, it’s not a huge problem.

So, what you need to do, in order to test the whole thing is to change your logout code to:

// @/store/actions.js

const actions = {
    // some actions here
    async logout({ dispatch }) {
        // Solution 1: `require()`
        const Vue = require("vue").default;
        // Solution 2 : `import()` which is not a big deal here since Vuex' actions always return a promise
        const Vue = await import("vue").then(module => module.default);
        await Vue.auth.logout();
        return dispatch("resetSession"); // Perform some cleanup here
    }
    // some more actions here
}

export { actions };

One last gotcha: you probably have noticed require("vue").default and import("vue").then(module => module.default) in dynamic imports. It is because since the release of Webpack 2, default exports are not directly returned when you require() or import(). Instead, you get a Webpack module object with a default property and other non-default export (see here).

So, when you mock the module, you have to expose an object that has the same topology:

import { applicationInit } from "@/application-init";
import { createLocalVue } from "@vue/test-utils";

import i18n from "@/i18n";
import store from "@/store";
import router from "@/router";
import AwesomeComponUserMenuent from "@/components/UserMenu";

const localVue = applicationInit(createLocalVue());
jest.doMock("vue", () => {default: localVue});

That’s it. Now, you can enjoy your tests !