Testing Vue.js Components with Jest

By Alex Jover Morales
  • Instant online access to over 8,000+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies

About this book

Unit testing in modern component-based JavaScript frameworks is not straightforward. You need a test suite that is reliable and runs quickly. Components are connected to one another, and the browser adds a layer of UI, which makes everything inter-dependent while we test components in isolation. Jest is a fully-featured JavaScript testing framework that will do all your work for you.

This book shows you how to test Vue.js components easily and take advantage of the fully-featured Jest testing framework with the aid of practical examples. You'll learn the different testing styles and their structures. You'll also explore how your Vue.js components respond to various tests. You'll see how to apply techniques such as snapshot testing, shallow rendering, module dependency mocking, and module aliasing to make your tests smooth and clean.

By the end of this book, you'll know all about testing your components by utilizing the features of Jest.

Publication date:
October 2019
Publisher
Packt
Pages
88
ISBN
9781839219689

 

Write the First Vue.js Component Unit Test in Jest

The official VueJS testing library, vue-test-utils (https://github.com/vuejs/vue-test-utils), which is based on avoriaz (https://github.com/eddyerburgh/avoriaz), is just around the corner. Indeed, @EddYerburgh (https://twitter.com/EddYerburgh) is doing a very good job of creating it. This library provides all the necessary tooling to make writing unit tests in a VueJS application easy.

Jest (https://facebook.github.io/jest), on the other side, is a testing framework developed at Facebook, which makes testing a breeze using a number of awesome features, including the following:

  • Almost no configuration by default
  • A very cool interactive mode
  • Running tests in parallel
  • Testing with Spies, stubs, and mocks out of the box
  • Built-in code coverage
  • Snapshot testing
  • Module-mocking utilities

You've probably already written tests without using any of these tools, just by using Karma, Mocha, Chai, Sinon, and so on, but you'll see how much easier it can be with these tools.

Setting Up a vue-test Sample Project

Let's start by creating a new project using vue-cli (https://github.com/vuejs/vue-cli) and answering NO to all yes/no questions:

npm install -g vue-cli

vue init webpack vue-test

cd vue-test

Then, we'll need to install some dependencies, as follows:

# Install dependencies

npm i -D jest vue-jest babel-jest

jest-vue-preprocessor (https://github.com/vire/jest-vue-preprocessor) is required to make Jest understand .vue files, and babel-jest (https://github.com/facebook/jest/tree/master/packages/babel-jest) is required for integration with Babel.

Now install 'vue-test-utils' library.

npm i -D @vue/test-utils

Let's add the following Jest configuration in the package.json:

{

  "jest": {

    "moduleNameMapper": {

      "^vue$": "vue/dist/vue.common.js"

    },

    "moduleFileExtensions": ["js", "vue"],

    "transform": {

      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",

      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"

    }

  }

}

moduleFileExtensions will tell Jest which extensions to look for, and transform will tell Jest which preprocessor to use for a file extension.

Finally, add a test script to the package.json:

{

  "scripts": {

    "test": "jest"

  }

}

Testing a Component

I'll be using single-file components here, and I haven't checked whether splitting them into their own HTML, CSS, or js files works or not, so let's assume you're doing that as well.

First, create a MessageList.vue component under src/components:

<template>

  <ul>

    <li v-for="message in messages">

      {{ message }}

    </li>

  </ul>

</template>

<script>

  export default {

    name: "list",

    props: ["messages"]

  };

</script>

And then update App.vue to use it as follows:

<template>

  <div id="app">

    <MessageList :messages="messages" />

  </div>

</template>

<script>

  import MessageList from "./components/MessageList";

  export default {

    name: "app",

    data: () => ({ messages: ["Hey John", "Howdy Paco"] }),

    components: {

      MessageList

    }

  };

</script>

We already have a couple of components that we can test. Let's create a test folder under the project root and an App.test.js file:

import Vue from "vue";

import App from "../src/App";

describe("App.test.js", () => {

  let cmp, vm;

  beforeEach(() => {

    cmp = Vue.extend(App); // Create a copy of the original component

    vm = new cmp({

      data: {

        // Replace data value with this fake data

        messages: ["Cat"]

      }

    }).$mount(); // Instances and mounts the component

  });

  it('equals messages to ["Cat"]', () => {

    expect(vm.messages).toEqual(["Cat"]);

  });

});

Now, if we run npm test (or npm t as a shorthand version), the test should run and pass. Since we're modifying the tests, let's run it in watch mode:

npm t -- --watch

The Problem with Nested Components

This test is too simple. Let's check that the output is expected as well. For that, we can use the amazing Snapshot feature of Jest, which will generate a snapshot of the output and check it against the upcoming runs. Add after the previous it in App.test.js:

it("has the expected html structure", () => {

  expect(cmp.element).toMatchSnapshot();

});

This will create a test/__snapshots__/App.test.js.snap file. Let's open it and inspect it:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports['App.test.js has the expected html structure 1'] = '

<div id="app">

  <ul>

    <li>

      Cat

    </li>

  </ul>

</div>

';

If you don't know very much about Snapshot, don't worry; I'll cover it in more depth in Chapter 9, Snapshot Testing.

In case you haven't noticed, there is a big problem here: the MessageList component has been rendered as well. Unit tests must be tested as independent units, meaning that in App.test.js, we want to test the App component and not have to care about anything else at all.

This can be the cause of several problems. Imagine, for example, that the children components (MessageList, in this case) perform side-effect operations on the created hook, such as the calling of fetch, there being a Vuex action, or a change of state. That's something we definitely don't want.

Luckily, shallow rendering solves this nicely.

What Is Shallow Rendering?

Shallow rendering (http://airbnb.io/enzyme/docs/api/shallow.html) is a technique that ensures that your component is rendering without children. This is useful for:

  • Testing only the component you want to test (that's what unit tests stand for)
  • Avoiding side effects that children components can have, such as making HTTP calls, calling store actions, and so on

Testing a Component with Vue-Test-Utils

vue-test-utils provides us with shallow rendering, among other features. We could rewrite the previous test as follows:

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

import App from "../src/App";

describe("App.test.js", () => {

  let cmp;

  beforeEach(() => {

    cmp = shallowMount(App, {

      // Create a shallow instance of the component

      data: {

        messages: ["Cat"]

      }

    });

  });

  it('equals messages to ["Cat"]', () => {

    // Within cmp.vm, we can access all Vue instance methods

    expect(cmp.vm.messages).toEqual(["Cat"]);

  });

  it("has the expected html structure", () => {

    expect(cmp.element).toMatchSnapshot();

  });

});

And now, if you're still running Jest in watch mode, you'll see that the test still passes, but the Snapshot doesn't match. Press u to regenerate it. Then, open and inspect it again:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports['App.test.js has the expected html structure 1'] = '

<div id="app">

  <!-- -->

</div>

';

Do you see? Now, no children have been rendered and we tested the App component fully isolated from the component tree. Also, if you have any created or other hooks in the children's components, they haven't been called either.

If you're curious about how shallow rendering is implemented, check out the source code (https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/shallow-mount.js) and you'll see that it is basically stubbing the components key, the render method, and the life cycle hooks.

In the same vein, you can implement the MessageList.test.js test as follows:

import { mount } from '@vue/test-utils'

import MessageList from '../src/components/MessageList'

describe('MessageList.test.js', () => {

  let cmp

  beforeEach(() => {

    cmp = mount(MessageList, {

      // Be aware that props is overridden using 'propsData'

      propsData: {

        messages: ['Cat']

      }

    })

  })

  it('has received ['Cat'] as the message property', () => {

    expect(cmp.vm.messages).toEqual(['Cat'])

  })

  it('has the expected html structure', () => {

    expect(cmp.element).toMatchSnapshot()

  })

})

You can find the full example from this chapter on GitHub (https://github.com/alexjoverm/vue-testing-series/tree/lesson-1).

 

Setting Up a vue-test Sample Project

Let's start by creating a new project using vue-cli (https://github.com/vuejs/vue-cli) and answering NO to all yes/no questions:

npm install -g vue-cli

vue init webpack vue-test

cd vue-test

Then, we'll need to install some dependencies, as follows:

# Install dependencies

npm i -D jest vue-jest babel-jest

jest-vue-preprocessor (https://github.com/vire/jest-vue-preprocessor) is required to make Jest understand .vue files, and babel-jest (https://github.com/facebook/jest/tree/master/packages/babel-jest) is required for integration with Babel.

Now install 'vue-test-utils' library.

npm i -D @vue/test-utils

Let's add the following Jest configuration in the package.json:

{

  "jest": {

    "moduleNameMapper": {

      "^vue$": "vue/dist/vue.common.js"

    },

    "moduleFileExtensions": ["js", "vue"],

    "transform": {

      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",

      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"

    }

  }

}

moduleFileExtensions will tell Jest which extensions to look for, and transform will tell Jest which preprocessor to use for a file extension.

Finally, add a test script to the package.json:

{

  "scripts": {

    "test": "jest"

  }

}

 

Testing a Component

I'll be using single-file components here, and I haven't checked whether splitting them into their own HTML, CSS, or js files works or not, so let's assume you're doing that as well.

First, create a MessageList.vue component under src/components:

<template>

  <ul>

    <li v-for="message in messages">

      {{ message }}

    </li>

  </ul>

</template>

<script>

  export default {

    name: "list",

    props: ["messages"]

  };

</script>

And then update App.vue to use it as follows:

<template>

  <div id="app">

    <MessageList :messages="messages" />

  </div>

</template>

<script>

  import MessageList from "./components/MessageList";

  export default {

    name: "app",

    data: () => ({ messages: ["Hey John", "Howdy Paco"] }),

    components: {

      MessageList

    }

  };

</script>

We already have a couple of components that we can test. Let's create a test folder under the project root and an App.test.js file:

import Vue from "vue";

import App from "../src/App";

describe("App.test.js", () => {

  let cmp, vm;

  beforeEach(() => {

    cmp = Vue.extend(App); // Create a copy of the original component

    vm = new cmp({

      data: {

        // Replace data value with this fake data

        messages: ["Cat"]

      }

    }).$mount(); // Instances and mounts the component

  });

  it('equals messages to ["Cat"]', () => {

    expect(vm.messages).toEqual(["Cat"]);

  });

});

Now, if we run npm test (or npm t as a shorthand version), the test should run and pass. Since we're modifying the tests, let's run it in watch mode:

npm t -- --watch

The Problem with Nested Components

This test is too simple. Let's check that the output is expected as well. For that, we can use the amazing Snapshot feature of Jest, which will generate a snapshot of the output and check it against the upcoming runs. Add after the previous it in App.test.js:

it("has the expected html structure", () => {

  expect(cmp.element).toMatchSnapshot();

});

This will create a test/__snapshots__/App.test.js.snap file. Let's open it and inspect it:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports['App.test.js has the expected html structure 1'] = '

<div id="app">

  <ul>

    <li>

      Cat

    </li>

  </ul>

</div>

';

If you don't know very much about Snapshot, don't worry; I'll cover it in more depth in Chapter 9, Snapshot Testing.

In case you haven't noticed, there is a big problem here: the MessageList component has been rendered as well. Unit tests must be tested as independent units, meaning that in App.test.js, we want to test the App component and not have to care about anything else at all.

This can be the cause of several problems. Imagine, for example, that the children components (MessageList, in this case) perform side-effect operations on the created hook, such as the calling of fetch, there being a Vuex action, or a change of state. That's something we definitely don't want.

Luckily, shallow rendering solves this nicely.

What Is Shallow Rendering?

Shallow rendering (http://airbnb.io/enzyme/docs/api/shallow.html) is a technique that ensures that your component is rendering without children. This is useful for:

  • Testing only the component you want to test (that's what unit tests stand for)
  • Avoiding side effects that children components can have, such as making HTTP calls, calling store actions, and so on
 

Testing a Component with Vue-Test-Utils

vue-test-utils provides us with shallow rendering, among other features. We could rewrite the previous test as follows:

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

import App from "../src/App";

describe("App.test.js", () => {

  let cmp;

  beforeEach(() => {

    cmp = shallowMount(App, {

      // Create a shallow instance of the component

      data: {

        messages: ["Cat"]

      }

    });

  });

  it('equals messages to ["Cat"]', () => {

    // Within cmp.vm, we can access all Vue instance methods

    expect(cmp.vm.messages).toEqual(["Cat"]);

  });

  it("has the expected html structure", () => {

    expect(cmp.element).toMatchSnapshot();

  });

});

And now, if you're still running Jest in watch mode, you'll see that the test still passes, but the Snapshot doesn't match. Press u to regenerate it. Then, open and inspect it again:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports['App.test.js has the expected html structure 1'] = '

<div id="app">

  <!-- -->

</div>

';

Do you see? Now, no children have been rendered and we tested the App component fully isolated from the component tree. Also, if you have any created or other hooks in the children's components, they haven't been called either.

If you're curious about how shallow rendering is implemented, check out the source code (https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/shallow-mount.js) and you'll see that it is basically stubbing the components key, the render method, and the life cycle hooks.

In the same vein, you can implement the MessageList.test.js test as follows:

import { mount } from '@vue/test-utils'

import MessageList from '../src/components/MessageList'

describe('MessageList.test.js', () => {

  let cmp

  beforeEach(() => {

    cmp = mount(MessageList, {

      // Be aware that props is overridden using 'propsData'

      propsData: {

        messages: ['Cat']

      }

    })

  })

  it('has received ['Cat'] as the message property', () => {

    expect(cmp.vm.messages).toEqual(['Cat'])

  })

  it('has the expected html structure', () => {

    expect(cmp.element).toMatchSnapshot()

  })

})

You can find the full example from this chapter on GitHub (https://github.com/alexjoverm/vue-testing-series/tree/lesson-1).

About the Author

  • Alex Jover Morales

    Alex Jover Morales is a Vue.js core team partner. He co-organizes Alicante Frontend and Vue Day. He is an instructor at Alligatorio and is interested in web performance, PWA, code quality, and the human side of code.

    Browse publications by this author
Book Title
Unlock this full book with a FREE 10-day trial
Start Free Trial