Testing Vue.js Components with Vue Test Utils

Testing Vue.js Components with Vue Test Utils

Software testing is the process of evaluating and verifying that a software product or application runs successfully and performs its required tasks without any errors. Testing might seem like a waste of time to some developers, but it is important to test your application or components if you want to build a well-functioning application.

Vue Test Utils (VTU) is a set of utility functions aimed at simplifying the testing of Vue.js components. It provides methods to mount and interact with Vue components in an isolated manner. Since the migration from Vue 2 to Vue 3, there is a new version of Vue Test Utils specifically for Vue 3.

In this article, we will build a sample application using a test-first approach. We will cover some use cases of Vue Test Utils for testing Vue components and how to write an easy-to-test component with Vue Test Utils.

What is Vue Test Utils

According to the documentation, Vue Test Utils (VTU) is a set of helper functions designed to simplify testing Vue.js components. This library provides a method for mounting Vue components and interacting with them in an isolated manner. These methods can be referred to as a wrapper. In essence, a wrapper is an abstraction of the mounted component. It provides some utility functions that make testing easier.

While pushing code into production, it's crucial to avoid introducing bugs. Perhaps, if you mistakenly pushed a bug to production, you'd want to be notified before it hits the production servers. Properly testing your codebase will enable you to fix the problem before any damage is done. So, it is important to test your codebase if you're building a project for the long run.

Setting Up Our Testing Environment

To get started, create a new folder and initialize a Vue.js project by running the command below:

$ npm init vue@latest

The command above will install and execute create-vue, the official Vue project scaffolding tool. Then, it will show prompts for several installation features. Select the following options below:

✔ Project name: ... <your-project-name>
✔ Add TypeScript? ... No / Yes
✔ Add JSX Support? ... No / Yes
✔ Add Vue Router for Single Page Application development? ... No / Yes
✔ Add Pinia for state management? ... No / Yes
✔ Add Vitest for Unit testing? ... No / Yes
✔ Add Cypress for both Unit and End-to-End testing? ... No / Yes
✔ Add ESLint for code quality? ... No / Yes
✔ Add Prettier for code formatting? ... No / Yes

Select "Yes" for "ESLint", "Prettier", and "JSX support" options, and "No" for the rest. Then, run the following command to install dependencies and start the dev server:

$ cd <your-project-name>
$ npm install
$ npm run dev

Now, to add Vue Test Utils, run the following command:

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

# or

$ yarn add --dev @vue/test-utils

Vue Test Utils is essentially framework-agnostic - you can use it with whichever test runner you like. The easiest way to try it out is using Jest, a popular test runner, which we will be using for this tutorial.

To load .vue files with Jest, you will need vue-jest. To install vue-jest, run the following command:

$ npm install --save-dev vue-jest

Then install Jest:

$ npm install --save-dev jest

Create a new file in your project folder called jest.config.js and add the following code:

module.exports = {
  preset: "ts-jest",
  globals: {},
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.vue$": "vue-jest",
    "^.+\\js$": "babel-jest",
  },
  moduleFileExtensions: ["vue", "js", "json", "jsx", "ts", "tsx", "node"],
};

If you don't want to configure it yourself, you can follow along by cloning the repository for this tutorial on GitHub here.

Writing Your First Test

Let's walk through how to use Vue Test Utils by writing a test for a simple demo application. Open App.vue and create a demo todo component:

<template>
  <div></div>
</template>

<script lang="ts">
export default {
  name: "TodoApp",

  data() {
    return {
      todos: [
        {
          id: 1,
          text: "Go to the grocery store",
          completed: false,
        },
      ],
    };
  },
};
</script>

After that, let's write our first test to verify a todo is rendered. Open the App.spec.ts and add the following test:

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

test("renders a todo", () => {
  const wrapper = mount(App);

  const todo = wrapper.get('[data-test="todo"]');

  expect(todo.text()).toBe("Go to the grocery store");
});

In the test above, we use the mount from VTU to render the component. Then we call mount and pass the component as the first argument. Essentially, we are finding an element with the selector data-test="todo" - in the DOM, this will look like <div data-test="todo">...</div>. Next, we call the text method to get the content, which we expect to be "Go to the grocery store".

At the moment, if we run the test, it will fail with the following error message:

  FAIL  src/App.spec.ts
  ✕ renders a todo (13ms)

  ● renders a todo
  
     Unable to get [data-test="todo"] within: <div></div>

The test failed because we aren't rendering any todo item, so the get() call is failing to return a wrapper. Now, let's update <template> in the App.vue to render the todos array:

<template>
  <div>
    <div v-for="todo in todos" :key="todo.id" data-test="todo">
      {{ todo.text }}
    </div>
  </div>
</template>

With this change above, the test will pass:

PASS  src/App.spec.ts
  ✓ renders a todo (24ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.092s
Ran all test suites.
✨  Done in 5.51s.

You just wrote your first component test successfully.

As a next step, we will add the ability for the user to create a new todo. To achieve that, we need a form with an input for the user to type some text. So when the user submits the form, we expect the todo to be rendered. To do that, take a look at the test below:

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

test("creates a todo", async () => {
  const wrapper = mount(App);

  await wrapper.get('[data-test="new-todo"]').setValue("New todo");
  await wrapper.get('[data-test="form"]').trigger("submit");

  expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2);
});

In the above test, we make use of the mount method to render the element. We also asserted that only 1 todo is rendered - this provides better clarification that we are adding an additional todo, as the final line of the test suggests. To update the input, we use the setValue method - this enables us to set the input's value.

Then, after updating the input, we make use of the trigger method to simulate the user submitting the form, and then we assert the number of to-do items has increased from 1 to 2.

If you run the test, it will fail. So, update App.vue to include the form and input elements to make the test pass:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

<template>
  <div>
    <div v-for="todo in todos" :key="todo.id" data-test="todo">
      {{ todo.text }}
    </div>

    <form data-test="form" @submit.prevent="createTodo">
      <input data-test="new-todo" v-model="newTodo" />
    </form>
  </div>
</template>

<script lang="ts">
export default {
  name: "TodoApp",

  data() {
    return {
      newTodo: "",
      todos: [
        {
          id: 1,
          text: "Go to the grocery store",
          completed: false,
        },
      ],
    };
  },

  methods: {
    createTodo() {
      this.todos.push({
        id: 2,
        text: this.newTodo,
        completed: false,
      });
    },
  },
};
</script>

In the code above, we make use of the v-model to bind to the input and @submit to listen to the form submission. When the form is submitted, createTodo is called and inserts a new todo into the todos array.

PASS  src/App.spec.ts
  ✓ creates a todo (19ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.185s, estimated 2s
Ran all test suites.
✨  Done in 4.71s.

Conditional Rendering

VTU (Vue Test Utils) provides a variety of features for rendering and asserting a component's state to ensure it is behaving correctly. Next, let's examine how to render components, as well as how to ensure that their content is correctly rendered.

A key feature of Vue is its dynamic insertion/removal of elements using v-if. So, let's look at how to test a component that uses v-if:

const Nav = {
  template: `
  <nav>
     <a id="user-profile" href="/profile">User Profile</a>
     <a v-if="subscribed" id="subscribed" href="/dashboard">User Dashboard</a>
  </nav>
    `,

  data() {
    return {
      subscribed: false,
    };
  },
};

In the <Nav> component above, a link to the user's profile is shown. Meanwhile, if the subscribed value is true, we will reveal a link to the user dashboard. So, there are three scenarios that we must verify are functioning correctly:

  • The /profile link should be shown.
  • When the user is subscribed, the /dashboard link should be shown.
  • When the user is not subscribed, the /dashboard link should not be shown.

The Vue Test Utils wrapper has a get() method that searches for an existing element. It makes use of the querySelector syntax. We assign the user dashboard link content by using get():

test("renders the user dashboard link", () => {
  const wrapper = mount(Nav);

  const userProfileLink = wrapper.get("#user-profile");
  expect(userProfileLink.text()).toEqual("User Profile");
});

Perhaps if the get() method does not return an element matching the selector, it will throw an error, and your test will fail. The get() method returns a DOMWrapper if an element is found.

However, the get method works on the assumption that elements do exist and throws an error when they do not. It is not recommended to use it for asserting existence. Therefore, the Vue Test Utils provides us with the find and exists methods. The next test asserts that if subscribed is false (which by default, it is), the user dashboard link is not available:

test("does not render user dashboard link", () => {
  const wrapper = mount(Nav);

  // Using `wrapper.get` would throw and make the test fail.
  expect(wrapper.find("#subscribed").exists()).toBe(false);
});

In the test above, you will notice we called the exists() method on the value returned from .find(). That's because find(), like mount(), also returns a wrapper. You can find a list of other methods supported for conditional rendering here.

How to Test Emitted Events

In Vue, components interact with each other through props and by emitting events by calling $emit. Now, let's take a look at how to verify events that are correctly emitted using the emitted() function provided by Vue Test Utils.

Let's examine this simple Counter component below. It has a button that, when clicked, increments an internal count variable and emits its value:

const Counter = {
  template: '<button @click="handleClick">Increment</button>',
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count += 1;
      this.$emit("increment", this.count);
    },
  },
};

To test the component above, we must verify that an increment event with the latest count value is emitted.

To do so, we will depend on the emitted() method. It will return an object with all the events the component has emitted, and their arguments in an array. Let's take a look at how it works:

test("emits an event when clicked", () => {
  const wrapper = mount(Counter);

  wrapper.find("button").trigger("click");
  wrapper.find("button").trigger("click");

  expect(wrapper.emitted()).toHaveProperty("increment");
});

In the test above, the first thing you will notice is that emitted() returns an object, where each key matches an emitted event. In this case, increment.

This test should pass now. We made sure we emitted an event with the correct name. If you haven't seen the trigger() method before, don't worry; it's used to simulate user interaction. You can learn about asserting complex events here.

How to Test Forms

In this section, we will explore ways to interact with form elements, set values, and trigger events.

Let's take a look at this basic form below:

<template>
  <div>
    <input type="email" v-model="email" />

    <button @click="submit">Submit</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      email: "",
    };
  },
  methods: {
    submit() {
      this.$emit("submit", this.email);
    },
  },
};
</script>

One of the most common ways to bind and input data in Vue is by using the v-model. So, to change the value of the input in VTU, you can use the setValue method. It accepts a parameter, most often a string or boolean, and returns a promise, which resolves after Vue has updated the DOM.

test("sets the value", async () => {
  const wrapper = mount(Component);
  const input = wrapper.find("input");

  await input.setValue("[email protected]");

  expect(input.element.value).toBe("[email protected]");
});

In the test above, you can see setValue sets the value property on the input elements to what we pass to it. We make use of await to ensure Vue has completed updating and the change has been reflected in the DOM before we make any assertions.

Triggering events is the second most important action when working with forms and action elements. Now, let's take a look at our button from the previous example.

<button @click="submit">Submit</button>

In order to trigger a click event, we can use the trigger() method.

test("trigger", async () => {
  const wrapper = mount(Component);

  // Trigger the element
  await wrapper.find("button").trigger("click");

  // Assert some action has been performed, like an emitted event.
  expect(wrapper.emitted()).toHaveProperty("submit");
});

In the test above, we trigger the click event listener so that the component executes the submit method. As we did with setValue, we use await to ensure the action is being reflected by Vue. We can then assert that some action has occurred. In this case, we emitted the right event.

So let's combine these two to test whether our simple form is emitting the user inputs:

test("emits the input to its parent", async () => {
  const wrapper = mount(Component);

  // Set the value
  await wrapper.find("input").setValue("[email protected]");

  // Trigger the element
  await wrapper.find("button").trigger("click");

  // Assert the `submit` event is emitted,
  expect(wrapper.emitted("submit")[0][0]).toBe("[email protected]");
});

To learn more about triggering complex event listeners and testing complex input components with VTU, read more here.

Vue Test Utils helps you write tests for Vue components; however, there is only so much VTU can do. You need to always write code that is easier to test and write tests that are meaningful and simple to maintain. You can follow this guide on how to write components that are easy to test:

Do Not Test Implementation Details: Always think in terms of inputs and outputs from a user perspective. Inputs – such as interaction, props, data streams, and outputs such as – DOM elements, events, and side effects.

Build Smaller, Simpler Components: There is a general rule of thumb that if a component does less, then it will be easier to test. Building smaller components will make them more composable and easier to understand.

Write a test before writing the component: If you write a test beforehand, you can't write untestable code. That's one of the great benefits of writing tests before writing components.

Conclusion

I hope you enjoyed reading through this tutorial. We have explored the Vue Test Utils specifically built for Vue3. We demonstrated some of the use-cases of Vue Test Utils for testing Vue components and took a look at how to set up a testing environment with VTU, how to do conditional rendering, and how to test emitted events and form elements.

Last Updated: May 31st, 2023
Was this article helpful?
Project

React State Management with Redux and Redux-Toolkit

# javascript# React

Coordinating state and keeping components in sync can be tricky. If components rely on the same data but do not communicate with each other when...

David Landup
Uchechukwu Azubuko
Details

Getting Started with AWS in Node.js

Build the foundation you'll need to provision, deploy, and run Node.js applications in the AWS cloud. Learn Lambda, EC2, S3, SQS, and more!

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms