{{announcement.body}}
{{announcement.title}}

Knowing What To Test — Vue Component Unit Testing

DZone 's Guide to

Knowing What To Test — Vue Component Unit Testing

A tutorial for reviewing components and knowing what to test and how frequently they need testing. Includes reviewing inputs and outputs.

· Performance Zone ·
Free Resource

A gorilla scratching their head.

Do you know what to test?
You may also like: How and Why We Moved to Vue.js

The most common question about unit testing Vue components I see out there is "what exactly should I test?"

While it's possible to test either too much or too little, my observation is that developers will usually err on the side of testing too much. After all, no one wants to be the guy or girl whose under-tested component crashed the app in production.

In this article, I'll share with you some guidelines I use for unit testing components that ensure I don't spend forever writing tests but provide enough coverage to keep me out of trouble.

I'll assume you've already had an introduction to Jest and Vue Test Utils.

Example Component

Before we get to the guidelines, let's first get familiar with the following example component that we'll be testing. It's called Item.vue and is a product item in an eCommerce app.

Image title

Here's the component's code. Note there are three dependencies: Vuex ($store), Vue Router ($router) and Vue Auth ($auth).

Item.vue

<template>
  <div>
    <h2>{{ item.title }}</h2>
    <button @click="addToCart">Add To Cart</button>
    <img :src="item.image"/>
  </div>
</template>
<script>
export default {
  name: "Item",
  props: [ "id" ],
  computed: {
    item () {
      return this.$store.state.find(
        item => item.id === this.id
      );
    }
  },
  methods: {
    addToCart () {
      if (this.$auth.check()) {
        this.$store.commit("ADD_TO_CART", this.id);
      } else {
        this.$router.push({ name: "login" });
      }
    }
  }
};
</script>


Spec File Setup

Here's the spec file for the tests. In it, we'll shallow mount our components with Vue Test Utils, so I've imported that, as well as the Item component we're testing.

I've also created a factory function that will generate an overrideable config object, saving us having to specify props and mocking the three dependencies in each test.

item.spec.js

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

function createConfig (overrides) {
  const id = 1;
  const mocks = {
    // Vue Auth
    $auth: {
      check: () => false
    },
    // Vue Router
    $router: {
      push: () => {}
    },
    // Vuex
    $store: {
      state: [ { id } ],
      commit: () => {}
    }
  };
  const propsData = { id };
  return Object.assign({ mocks, propsData }, overrides);
}

describe("Item.vue", () => {
  // Tests go here
});


Identify the Business Logic

The first and most important question to ask about a component you want to test is "what is the business logic?", in other words, what is the component meant to do?

For Item.vue, here is the business logic:

  • It will display an item based on the id prop received.
  • If the user is a guest, clicking the Add to Cart button redirects them to the login page.
  • If the user is logged in, clicking the Add to Cart button will trigger a Vuex mutation ADD_TO_CART.

Identify the Inputs and Outputs

When you unit test a component, you treat it as a black box. Internal logic in methods, computed properties, etc, only matter insofar as they affect output.

So, the next important thing is to identify the inputs and outputs of the component, as these will also be the inputs and outputs of your tests.

In the case of Item.vue, the inputs are:

  • id prop.
  • State from Vuex and Vue Auth.
  • User input via button clicks.

While the outputs are:

  • Rendered markup.
  • Data sent to Vuex mutation or Vue Router push.

Some components may also have forms and events as inputs, and emit events as outputs.

Test One: Router Called When Guest Clicks Button

One piece of business logic is "If the user is a guest, clicking the Add to Cart button redirects them to the login page". Let's write a test for that.

We'll set up the test by shallow mounting the component, then finding and clicking the Add to Cart button.

test("router called when guest clicks button", () => {
  const config = createConfig();
  const wrapper = shallowMount(Item, config);
  wrapper
    .find("button")
    .trigger("click");
  // Assertion goes here
}


We'll add an assertion in a moment.

Don't Go Beyond the Boundaries of the Input and Output

It'd be tempting in this test to check that the route changed to that of the login page after clicking the button e.g.

import router from "router";

test("router called when guest clicks button", () => {
  ...
  // Wrong
  const route = router.find(route => route.name === "login");
  expect(wrapper.vm.$route.path).toBe(route.path);
}


While this does test the component output implicitly, it's relying on the router to work, which should not be the concern of this component.

It's better to directly test the output of this component, which is the call to $router.push. Whether the router completes that operation is beyond the scope of this particular test.

So let's spy on the push method of the router, and assert that it gets called with the login route object.

import router from "router";

test("router called when guest clicks button", () => {
  ...
  jest.spyOn(config.mocks.$router, "push");
  const route = router.find(route => route.name === "login");
  expect(spy).toHaveBeenCalledWith(route);
}


Test Two: Vuex Called When Auth User Clicks Button

Next, let's test the business logic for "If the user is logged in, clicking the Add to Cart button will trigger a Vuex mutation ADD_TO_CART".

To re-iterate the above lesson, you don't need to check if the Vuex state gets modified. We would have a separate test for the Vuex store to verify that.

This component's job is simply to make the commit, so we just need to test it does it that.

So let's first override the $auth.check mock so it returns true (as it would for a logged-in user). We'll then spy on the commit method of the store, and assert it was called after the button is clicked.

test("vuex called when auth user clicks button", () => {
  const config = createConfig({
    mocks: {
      $auth: {
        check: () => true
      }
    }
  });
  const spy = jest.spyOn(config.mocks.$store, "commit");
  const wrapper = shallowMount(Item, config);
  wrapper
    .find("button")
    .trigger("click");
  expect(spy).toHaveBeenCalled();
}


Don't Test Functionality of Other Libraries

The Item component displays a store item's data, specifically the title, and image. Maybe we should write a test to specifically check these? For example:

test("renders correctly", () => {
  const wrapper = shallowMount(Item, createConfig());
  // Wrong
  expect(wrapper.find("h2").text()).toBe(item.title);
}


This is another unnecessary test as it's just testing Vue's ability to take in data from Vuex and interpolate it in the template. The Vue library already has tests for that mechanism so you should rely on that.

Test Three: Renders Correctly

But hang on, what if someone accidentally renames title to name and then forgets to update the interpolation? Isn't that something worth testing for?

Yes, but if you test every aspect of your templates like this, where do you stop?

The best way to test markup is to use a snapshot test to check the overall rendered output. This will cover not just the title interpolation, but also the image, the button text, any classes, etc.

test("renders correctly", () => {
  const wrapper = shallowMount(Item, createConfig());
  expect(wrapper).toMatchSnapshot();
});


Here are some examples of other things there is no need to test:

  • If the src property is bound to the img element.
  • If data added to the Vuex store is the same data that gets interpolated.
  • If the computed property returns the correct item.
  • If the router push redirects to the correct page.

Wrap Up

I think those three relatively simple tests are sufficient for this component.

A good mindset to have when unit testing components is to assume a test is unnecessary until proven otherwise.

Here are the questions you can ask yourself:

  • Is this part of the business logic?
  • Does this directly test the inputs and outputs of the component?
  • Is this testing my code, or third-party code?

Happy testing!


Further Reading

Vue Development in 2019: What You Need to Know

Create and Publish Web Components With Vue CLI 3

Vue.js Tutorial: Vue.js Development With Storybook and Applitools

Topics:
vuejs ,testing ,unit tests ,components ,spec file ,business logic ,inputs ,outputs ,performance

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}