5. Testing a Vue Component

Part 5 of 5 in our Testing like a Pro in JavaScript series.
Written by Alex Jover Morales
5. Testing a Vue Component

This testing series are focused on JavaScript as a language and testing techniques, but since we’re VueSchool, we couldn’t end it up without a simple Vue example, right?

We’re not diving into this part, but it’s a start for you to get setup quickly and to give you an initial state where you can continue on your own.

To start, let’s create a pretty simple Hello.vue component, which just prints some text based on a name local state variable:

<template>
  <div>Hello {{ name }}!</div>
</template>

<script>
export default {
  data() {
    return { name: "Vue" };
  },
};
</script>

Vue components are atomic pieces that render parts of the DOM based on some state, either internal or external. For that reason, it’s a perfect target to use snapshot testing, but we’ll get back to that later.

For now let’s setup a test that imports the Hello component and doesn’t do much more yet. We can use an expect(true).toBe(true) to bypass the test assertion:

// hello.test.js
import Hello from "./Hello";

describe("Hello component", () => {
  it("renders correctly", () => {
    expect(true).toBe(true);
  });
});

If you try to run this test, you’ll get the following error:

Cannot find module ‘./Hello’ from ‘hello.test.js’

You might think that it’s because it’s missing the .vue extension. Well, you can try to change that:

import Hello from "./Hello.vue";

But now you’ll get another error:

SyntaxError: Unexpected token {
it(“renders correctly”, () => {
const comp = new Vue(Hello).$mount();
expect(comp.$el).toMatchSnapshot();
});
});

As you can see, we can access the HTML output by accessing the instance property $el. If we inspect the snapshot created under the snapshots folder in the same level of the test, we’ll see the following snapshot being created:

exports[`Hello component renders correctly 1`] = `"Hello Vue!"`;

State based tests

Let’s add another test. Now we’re going to check that when we change the name variable on the local state, it gets udpated as expected. So for the following test:

it("renders correctly after changing the name state", () => {
    const comp = new Vue(Hello).$mount();
    comp.name = "Camel";
    expect(comp.$el).toMatchSnapshot();
})

It should output the snapshot with the Hello Camel! text, but if we inspect it we’ll realized that it’s incorrect:

exports[`Hello component renders correctly after changing the name state 1`] = `
<div>
  Hello Vue!
</div>
`;

Why hasn’t it been updated? Vue performs DOM updates asynchronously, so when we update the name state and we assert it, it hasn’t been propagated to the DOM yet.

To avoid this issue, we have to wait to the next update cycle by using the $nextTick instance method:

it("renders correctly after changing the name state", () => {
  const comp = new Vue(Hello).$mount();
  comp.name = "Camel";

  comp.$nextTick(() => {
    expect(comp.$el).toMatchSnapshot();
  });
});

Careful: even if you tried it and it seems like it works as expected because it passed, that’s not true. What’s really happening is that the test finishes without executing the expect function because it’s called at a later time asynchronously.

We’ve seen how we can use async/await in Jest for asynchronous calls, but that’s not useful in this case since $nextTick is callback based.

For callback based asynchronous tests, Jest allows to define a callback argument in its it function that we can call whenever we want, and Jest will wait until it’s called. So we can update the test to make it safe by providing a cb argument and calling it after the expect check:

it("renders correctly after changing the name state", done => {
  const comp = new Vue(Hello).$mount();
  comp.name = "Camel";

  comp.$nextTick(() => {
    expect(comp.$el).toMatchSnapshot();
    done();
  });
});

Make sure the done function is indeed called at some point in your tests, otherwise it will throw a timeout error.

Notice that I’ve called it *done**, but you can name it as you prefer.*

Props based tests

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!

As well as with the state, we can have different render states based on the props.
In order to pass props values to the props of a component, we have to use the propsData component option. But in the way we’re currently defining the component, we have no way to define propsData, because we’re just passing the component as an argument to Vue:

it("renders correctly with different props", () => {
  const comp = new Vue(Hello).$mount();
  expect(comp.$el).toMatchSnapshot();
});

Instead, what we can do is to extend the component so that we can add parameters such as propsData. In order to extend a component, we can make use of the Vue.extend method:

it("renders correctly setting the `greeter` prop", () => {
  const props = { greeter: "Joe" };
  const Constructor = Vue.extend(Hello);
  const comp = new Constructor({ propsData: props }).$mount();

  expect(comp.$el).toMatchSnapshot();
});

When we extend a component, we’re creating a Constructor of itself. In that way, we can create a new instance passing any component option we want to in the new Constructor({ propsData: props }) part.

When you run the tests, you’ll need to update the snapshots by pressing “u” because we changed the code of the component template.

The last snapshot should show the following message, with Joe as the greeter:

exports[`Hello component renders correctly setting the \`greeter\` prop 1`] = `
<div>
  Hello Vue from Joe!
</div>
`;

Bonus: using vue-test-utils

Edd Yerburgh from the Vue core team has develop vue-test-utils, a package with a set of utils that makes testing Vue components a breeze. It’s testing framework-agnostic so you could use it in mocha or any other tool of your choice.

Not only has helper methods for most of the common tasks in testing, but also allow us to easily apply shallow rendering, a technique to avoid rendering child components. Explaining this concept in detail is out of the scope of this series, but you can read more in this article I wrote some time ago.

In this particular case, we just have some simple tests, but let’s see how can it help us by writing the tests one by one using vue-test-utils.

Let’s start by installing it:

npm install --save-dev @vue/test-utils

Then, let’s rewrite the first test. We need to import either mount or shallow, depending if you want deep or shallow rendering, both returning a wrapper of your component with a set of additional methods. In this case it makes no difference since the Hello component has no children, so we’re going to use mount:

import Vue from "vue";
import { mount } from "@vue/test-utils";
import Hello from "./Hello.vue";

describe("Hello component", () => {
  it("renders correctly", () => {
    const comp = mount(Hello);
    expect(comp.html()).toMatchSnapshot();
  });
  //...

Just by passing the component to mount, we have the wrapper created. Instead of accessing the $el property, vue-test-utils give us some abstractions like the html() for getting the HTML rendered output.

Note that we’re not using Vue anymore, so we could remove it. I’m keeping it till the end just so other tests don’t fail until we rewrite them.

If you run the tests, you’ll see that is failing because the snapshot is different. But if you take a closer look, you’ll see that the output is correct, but the html() method performs some formatting, which is great:

Knowing that, we can safely update the snapshot by pressing the “u” key in case you’re still running the tests in watch mode.

Let’s go to the next test. Remember we had to use $nextTick to wait for the next DOM update to apply:

it("renders correctly after changing the name state", () => {
  const comp = new Vue(Hello).$mount();
  comp.name = "Camel";

  comp.$nextTick(() => {
    expect(comp.$el).toMatchSnapshot();
  });
});

For this case, vue-test-utils give as a setData() method for updating local state that internally forces a DOM update, so we don’t need to handle that on our own, making the test much simpler:

it("renders correctly after changing the name state", () => {
  const comp = mount(Hello);
  comp.setData({ name: "Camel" });
  expect(comp.html()).toMatchSnapshot();
});

Finally, let’s go through the last test:

it("renders correctly setting the `greeter` prop", () => {
  const props = { greeter: "Joe" };
  const Constructor = Vue.extend(Hello);
  const comp = new Constructor({ propsData: props }).$mount();

  expect(comp.$el).toMatchSnapshot();
});

We can take advantage of the second argument of the mount and shallow functions in order to pass any additional parameters, such as the propsData option:

it("renders correctly setting the `greeter` prop", () => {
  const comp = mount(Hello, {
    propsData: {
      greeter: "Joe",
    },
  });

  expect(comp.html()).toMatchSnapshot();
});

In that way, we don’t need to extend the component in order to add additional component options, we can do it right away.

Now that we’ve rewritten all the tests, we can remove the line where we import Vue, since it’s not needed anymore:

import Vue from "vue";

vue-test-utils have helper methods for manipulating props, trigger events, mock slots, and much more. Here we’ve just seen a very brief intro just so you know that Vue has its own official test utils that you can use to write real applications test suites.

Wrapping Up

In this series we’ve gone through the practices of testing in detail, starting from scratch to know what tests are, and progressively adding more concepts about testing, how to apply them in JavaScript and finishing up by scratching the surface of testing a Vue component.

I hope that if you were new to testing, you could follow this series as a whole, and if you had already some experience, you learnt something new from it.

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!


Article written by Alex Jover Morales

Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.