Chapter 6 – Testing Vue.js Components with Jest

Chapter 6

Test Methods and Mock Dependencies

What should we test in methods? That's a question that we had when we started doing unit tests. Everything comes down to testing what that method does and only that. This means we need to avoid calls to any dependency, so we'll need to mock them.

Let's add a submit event to the form in the Form.vue component that we created in the previous chapter:

<form @submit.prevent="onSubmit(inputValue)"></form>

The .prevent modifier is just a convenient way to call event.preventDefault() so that it doesn't reload the page. Now, make some modifications to call an API, and then store the result by adding a results array to the data as well as an onSubmit method:

export default {

  data: () => ({

    inputValue: "",

    results: []

  }),

  methods: {

    onSubmit(value) {

      axios

        .get("https://jsonplaceholder.typicode.com/posts?q=" + value)

        .then(results => {

          this.results = results.data;

        });

    }

  }

};

Here, the method is using axios to perform an HTTP call to the posts endpoint of jsonplaceholder, which is just a RESTful API for this kind of example. Additionally, with the q query parameter, we can search for posts using the value provided as a parameter.

For testing the onSubmit method:

  • We don't want to call axios.get actual method.
  • We want to check it is calling axios (but not the real one) and that it returns a promise.
  • That promise callback should set this.results to the promised result.

This is probably one of the hardest things to test when you have external dependencies, plus those return promises that do things inside. What we need to do is to mock the external dependencies.

Mocking External Module Dependencies

Jest provides a really great mocking system that allows you to mock everything in quite a convenient way. In fact, you don't need any extra libraries to do it. We have seen already jest.spyOn and jest.fn for spying and creating stub functions, although that's not enough for this case.

Here, we need to mock the whole axios module. That's where jest.mock comes into play. It allows us to easily mock module dependencies by writing at the top of your file:

jest.mock("dependency-path", implementationFunction);

You must know that jest.mock is hoisted, which means it will be placed at the top:

jest.mock("something", jest.fn);

import foo from "bar";

// ...

So, the preceding code is equivalent to this:

import foo from "bar";

jest.mock("something", jest.fn); // this will end up above all imports and everything

// ...

At the time of writing, I still haven't found much information about how to do in Jest what we're going to do here on the internet. Luckily, you don't have to go through the same struggle.

Let's write the mock for axios at the top of the Form.test.js test file and the corresponding test case:

jest.mock("axios", () => ({

  get: jest.fn()

}));

import { shallowMount } from "@vue/test-utils";

import Form from "../src/components/Form";

import axios from "axios"; // axios here is the mock from above!

// ...

it("Calls axios.get", () => {

  cmp.vm.onSubmit("an");

  expect(axios.get).toBeCalledWith(

    "https://jsonplaceholder.typicode.com/posts?q=an"

  );

});

This is great. We're indeed mocking axios, so the original axios is not called and neither is any HTTP called. And we're even checking, by using toBeCalledWith, that it's been called with the correct parameters. However, we're still missing something: we're not checking whether it returns a promise.

First, we need to make our mocked axios.get method to return a promise. jest.fn accepts a factory function as a parameter, so we can use it to define its implementation:

jest.mock("axios", () => ({

  get: jest.fn(() => Promise.resolve({ data: 3 }))

}));

However, we still cannot access the promise because we're not returning it. In testing, it is a good practice to return something from a function when possible, as it makes testing much easier. So, let's now do this in the onSubmit method of the Form.vue component:

export default {

  methods: {

    // ...

    onSubmit(value) {

      const getPromise = axios.get(

        "https://jsonplaceholder.typicode.com/posts?q=" + value

      );

      getPromise.then(results => {

        this.results = results.data;

      });

      return getPromise;

    }

  }

};

Then, we can use the very clean ES2017 async/await syntax in the test to check the promise result:

it("Calls axios.get and checks promise result", async () => {

  const result = await cmp.vm.onSubmit("an");

  expect(result).toEqual({ data: [3] });

  expect(cmp.vm.results).toEqual([3]);

  expect(axios.get).toBeCalledWith(

    "https://jsonplaceholder.typicode.com/posts?q=an"

  );

});

Here, you can see that we don't just check the promised result, but also that the results internal state of the component is updated, as expected, by doing expect(cmp.vm.results).toEqual([3]).

Keeping Mocks Externalized

Jest allows us to have all our mocks separated into their own JavaScript file by placing them under a __mocks__ folder and keeping the tests as clean as possible.

So, we can take the jest.mock... block from the top of the Form.test.js file out to its own file:

// test/__mocks__/axios.js

module.exports = {

  get: jest.fn(() => Promise.resolve({ data: [3] }))

};

Just like this, and with no extra effort, Jest automatically applies the mock in all our tests so that we don't have to do anything extra, or manually mock it in every test. Notice that the module name must match the filename. If you run the tests again, they should still pass.

Keep in mind that the modules registry and the mocks state are kept as they are, so, if you write another test afterward, you may get undesirable results:

it("Calls axios.get", async () => {

  const result = await cmp.vm.onSubmit("an");

  expect(result).toEqual({ data: [3] });

  expect(cmp.vm.results).toEqual([3]);

  expect(axios.get).toBeCalledWith(

    "https://jsonplaceholder.typicode.com/posts?q=an"

  );

});

it("Axios should not be called here", () => {

  expect(axios.get).toBeCalledWith(

    "https://jsonplaceholder.typicode.com/posts?q=an"

  );

});

The second test should fail, but it doesn't. That's because axios.get was called on the test before.

For that reason, it's good practice to clean the module registry and the mocks, since they're manipulated by Jest in order to make mocking happen. For that, you can add in your beforeEach:

beforeEach(() => {

  cmp = shallowMount(Form);

  jest.resetModules();

  jest.clearAllMocks();

});

This will ensure that each test starts with clean mocks and modules, as it should be in unit testing.

Wrapping Up

The Jest mocking feature, along with snapshot testing, are the two things I love most about Jest. That's because they make what is usually quite hard to test very easy, allowing you to focus on writing faster and better-isolated tests and keeping your code base bulletproof.

You can find all the code for this chapter on GitHub (https://github.com/alexjoverm/vue-testing-series/tree/Test-State-Computed-Properties-and-Methods-in-Vue-js-Components-with-Jest).