88. Setting up component tests


Resources:


The first PrintBay component we'll create tests for is our root component, App.vue. Let's go ahead and create a file that will house the tests that we write for it.

All our client unit tests are going to be in the folder "tests/unit/client", so let's go to the terminal and type:

$ touch tests/unit/client/App.spec.js

Before we open that file and start writing tests, there's two things I want to point out in the Jest config file.

Firstly, we see this transform property, showing us how our .vue files will be transformed with the "vue-jest" package. This is important because it allows us to load Vue single-file components into our Jest tests.

Secondly, you'll see we have this moduleNameMapper property, which is the same concept as Webpack aliases. This configuration allows us to use the same aliases in our Jest test files as we do in the rest of our app.

So with that in mind, open up the App test file, and we're now going to import our App component by putting:

tests/unit/client/App.spec.js

import App from "@/App";

Hopefully after seeing the Jest config you'll understand how it is we can import .vue file into a Jest test file, and, how it is we can use the @ symbol for aliased file names.

Moving on, let's import the mount method by going import { mount } from "@vue/test-utils";.

What to test

Let's now talk about what it is we're going to test for this component. Remember that unit tests assert an output for a given input. Now, App.vue doesn't have any inputs as it serves more of a structural role, and it's only job is really to display other components.

But we can and should still test the output of this component, in this case the rendered markup.

So let's begin by creating a describe block and we'll describe this test as "App.vue".

We'll then pass a function as the second argument, and inside that, we'll call it and create our first test. Let's call this test "should render correctly".

Inside the test, let's now mount our component and assign the wrapper to a variable by putting const wrapper = mount(App);.

tests/unit/client/App.spec.js

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

describe("App.vue", () => {
  it("should render correctly", () => {
    const wrapper = mount(App);
  });
});

Before we go any further, let's run this to make sure it's working so far. So we'll go to the terminal and put:

$ npm run test:unit:client

Router

As you can see, we're getting the error "cannot read property 'path' of undefined".

If we look at the App component file, you'll see why we get this error. We can conclude from the error message that this object, $route is not defined.

The likely explanation for this is that Vue Router has not been installed on the Vue instance we've used to mount the component in our test.

Importing router

So let's go ahead and import Vue Router into our test file by going to the top and putting import VueRouter from "vue-router";.

Normally, we'd now import Vue and put something like Vue.use(VueRouter), which would install Vue Router.

The problem with installing Vue Router this way, though, is that it adds it to the Vue constructor, meaning every future Vue instance created in the test will then get the router.

As you'll see later, sometimes we'd rather manually mock the router so we can set custom values for $route.path, and we won't be able to do that if we polluting the global Vue constructor this way.

Local Vue

Fortunately, Vue Test Utils offers a method called createLocalVue which provides a one-off Vue constructor that you can add plugins too without polluting the main Vue constructor.

So let's import createLocalVue from the Vue Test Utils package as well.

Now, at the top of our test, let's put const localVue = createLocalVue(); to create a local Vue constructor. We can then install the Vue Router plugin by going localVue.use(VueRouter);.

Next, we'll import the routes defined for this app at the top of the file by going import router from "@/router".

In order to install your custom routes, you would normally pass those to the Vue config object.

To do that with Vue Test Utils, you can pass a configuration object as an optional second argument to mount, so let's pass { localVue, router }.

tests/unit/client/App.spec.js

import App from "@/App";
import { mount, createLocalVue } from "@vue/test-utils";
import VueRouter from "vue-router";

describe("App.vue", () => {
  it("should render correctly", () => {
    const localVue = createLocalVue();
    localVue.use(VueRouter);
    const wrapper = mount(App, { localVue, router });
  });
});

With that done, we've now installed Vue Router on our test instance of Vue, solving the problem of the router global being undefined.

Let's run the test again and check if it works.

Shallow mounting

We're no longer seeing the error from before, but, we now have new errors, which hopefully means we're making progress.

Starting from the top, we have another type error "Cannot read property 'check' of undefined". Interestingly, this error is coming from the StickyNav component. Why would this component be affecting our App component test?

Looking at App.vue again, we can see StickyNav is a child component.

Now, we'd rather that App.vue doesn't load any of its child components when we test it, since in a unit test, we're aiming to test the unit in isolation, meaning we only want to test one component on it's own.

Vue Test Utils give us another method to do this called shallowMount. It's exactly the same as mount, except it auto-stubs the child components. If you don't know what auto-stubbing means, hang tight and I'll discuss it more in a moment.

First, to use shallowMount, we'll simply replace mount with it, firstly in the import statement, abd secondly in the test itself.

tests/unit/client/App.spec.js

import { shallowMount, createLocalVue } from "@vue/test-utils";
import VueRouter from "vue-router";
import App from "@/App";
import router from "@/router";

describe("App.vue", () => {
  it("should render correctly", () => {
    const localVue = createLocalVue();
    localVue.use(VueRouter);
    const wrapper = shallowMount(App, { localVue, router });
  });
});

With that done, let's run the test again.

This time it passes. We get quite a few "Unknown custom element" warnings, though.

Vuetify

You'll notice that all the warnings are for Vuetify components like v-app, v-container, v-content, and so on.

The reason for this is that in our app, Vuetify components are globally registered and so, again, our test Vue instance here doesn't know about them.

So, just like we installed Vue Router, we'll also need to install the Vuetify plugin which will globally register those components and solve that problem.

So let's begin by importing Vuetify at the top of the file by putting import Vuetify from "vuetify";.

Now, unfortunately, there's a design quirk in Vuetify that doesn't allow it to be added cleanly to localVue. I'll link to the GitHub issue documenting this problem above the video in case you want read more about this.

So, it's not ideal, but we're going to add Vuetify directly to the Vue constructor, exactly like I told you not to do early in this video.

Luckily, though, we won't have any tests where this is going to cause a problem, so don't lose any sleep over it.

So let's go to the top of the file and put import Vue from "vue", then add Vuetify to the Vue constructor by going Vue.use(Vuetify);.

...
import Vue from "vue";
Vue.use(Vuetify);
...

If you run the test again, you'll see it's now passing without errors or warnings.

In the next video, we'll add an assertion to this test.

Discussion

0 comments