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
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>
`;
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.
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.
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.