Guide to Unit Testing Vue Components

Last updated February 11th, 2020

This articles serves as a guide to unit testing Vue components.

We'll first look at why unit testing is important for creating maintainable software and what you should test. Then, we'll detail how to:

  1. Create and run a unit test for a Vue component
  2. Test different aspects of a Vue component
  3. Use mocks to test asynchronous functions
  4. Check the code coverage of your unit tests
  5. Structure your unit test files

If you're interested in learning more about Vue, check out my course: Learn Vue by Building and Deploying a CRUD App.

Dependencies:

  • Vue - JavaScript framework
  • Vue CLI - Tooling for Vue development
  • Jest - JavaScript testing framework
  • Node - JavaScript runtime

The source code (along with detailed installation instructions) for the Vue Weather App project that's used in this article can be found on GitLab: Vue Weather App.

Contents

Objectives

By the end of this article, you should be able to:

  1. Explain why unit testing is important
  2. Describe what you should (and should not) unit test
  3. Develop a unit test suite for a Vue component
  4. Run the unit tests for a Vue project using the Vue CLI
  5. Utilize the beforeEach() and afterEach() functions within a unit test suite
  6. Write unit tests for testing the implementation details of a Vue component
  7. Write unit tests for testing the behavioral aspects of a Vue component (click events, etc.)
  8. Understand how mocking helps to write unit tests
  9. Write unit tests for mocking a library and testing asynchronous functions
  10. Check the code coverage of your unit tests
  11. Develop a well-structured unit test file for testing a Vue component

Why Unit Test?

In general, testing helps ensure that your app will work as expected for your end users.

Software projects with high test coverage are never perfect, but it's a good initial indicator of the quality of the software. Additionally, testable code is generally a sign of a good software architecture, which is why advanced developers take testing into account throughout the entire development lifecycle.

Tests can be considered at three levels:

  1. Unit
  2. Integration
  3. End-to-end

Unit tests test the functionality of an individual unit of code isolated from its dependencies. They are the first line of defense against errors and inconsistencies in your codebase. Unit testing is a fundamental part of the Test-Driven Development (TDD) process.

Unit testing improves the maintainability of your code.

Maintainability refers to making bug fixes or enhancements to your code or to another developer needing to update your code at some point in the future.

Unit testing should be combined with a Continuous Integration (CI) process to ensure that your unit tests are constantly executing, ideally on each commit to your repository. A solid suite of unit tests can be critical to catching defects quickly and early in the development process before your end users come across them in production.

What to Test

What should you test? Or, more importantly: What should you not test?

There are three types of testing to consider with unit testing:

  1. Implementation details: the underlying business logic that a component uses to produce a result based on a given input
  2. Public interface/design contract: specific inputs (or props, in this case) produce specific results
  3. Side effects: "if this, then that"; i.e., when a button is clicked something happens.

Since you can't test all the things, what should you focus on?

Focus on testing inputs and outputs that the end user will interact with. The experience that the users of your product have is paramount!

  1. Inputs: data, props, user interaction, lifecycle methods, Vuex store, route params, query strings
  2. Outputs: rendered output, events, data results, Vuex store updates, dispatches

By focusing on testing the inputs/outputs of a software module (i.e., a Vue component), you are testing the key aspects that the end user will experience.

There may be other internal logic in a software module that is complex and needs to be unit tested, but these types of tests are most likely to need updating during a code refactor.

App Overview

Before discussing how to unit test Vue components, I want to give a short overview of the Vue Weather App that we'll be testing.

After cloning down the repo, install the dependencies and add the API key. Once done, the Vue Weather App can be run on your local computer by starting the development server:

$ npm run serve

Review the README for more info on creating and adding the API key from Open Weather.

Once the app is built, you will see a success message similar to:

 DONE  Compiled successfully in 2750ms                                                     1:34:53 PM


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://10.0.0.100:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

At this point, the development server will be up and running. You can see the Vue app by going to http://localhost:8080 in your favorite web browser. When you first load the app, no data is displayed; you'll just see an input field to enter the city that you'd like to get the weather for:

Vue Weather App Walkthrough - Step 1

Without any data entered, both the 'Search' and 'Clear' buttons are disabled.

Once you start entering data for a city (as soon as the first character is added), both the 'Search' and 'Clear' buttons will be enabled:

Vue Weather App Walkthrough - Step 2

If you click on 'Search' after entering a valid city, the weather data will be displayed for that city:

Vue Weather App Walkthrough - Step 3

At this point, clicking on the 'Clear Weather Data' button will cause the weather data to be cleared:

Vue Weather App Walkthrough - Step 4

However, the city that was entered will remain in the input field.

If you click on 'Clear' now, the input field will be cleared and the 'Search' and 'Clear' buttons will become disabled once again:

Vue Weather App Walkthrough - Step 5

What happens if you enter an invalid city? What if you click 'Clear' before 'Clear Weather Data'? What other states of the app can you find?

Unit Testing in Vue

Since components are the building blocks of Vue (and really any SPA framework, for that matter), they are the most crucial part of your app as a whole. So, spend the majority of your alloted testing time writing unit tests that test your app's components.

Based on my experience with unit testing Vue components, I've found that the testing tools and framework -- Vue Test Utils and Jest -- satisfy the key aspects of a good test environment:

  • tests are easy and fun to write
  • tests can be written quickly
  • tests can be executed with a single command
  • tests run quickly

Hopefully you'll find that unit testing your Vue components is an enjoyable experience, as I think that's key to encouraging more testing.

Unit Testing Tools

There are two tools that we're going to use for unit testing the Vue components:

  1. Vue Test Utils - the official unit testing utility library for Vue
  2. Jest - test runner for finding and executing the unit tests

Unit Testing Overview

For starters, let's discuss the naming convention for unit tests in Vue. The unit test file should be of the following format:

  • <Component Name>.spec.js

You should typically have one unit test file for each component in your Vue project. Within each unit test file, there can be a single unit test suite or multiple unit test suites.

The unit test files should be placed in a separate location from the Vue components, such as in the tests/unit folder:

$ tree -d -L 2
.
├── node_modules
├── public
├── src
│   ├── assets
│   └── components
└── tests
    └── unit

Running the Tests

The Vue CLI can be used to run the unit tests using Jest by executing:

$ npm run test:unit

> [email protected] test:unit vue-weather-app
> vue-cli-service test:unit

 PASS  tests/unit/Footer.spec.js
 PASS  tests/unit/Header.spec.js
 PASS  tests/unit/Banner.spec.js
 PASS  tests/unit/Weather.spec.js
 PASS  tests/unit/Search.spec.js
 PASS  tests/unit/App.spec.js

=============================== Coverage summary ===============================
Statements   : 89.36% ( 42/47 )
Branches     : 90% ( 9/10 )
Functions    : 94.12% ( 16/17 )
Lines        : 89.36% ( 42/47 )
================================================================================

Test Suites: 6 passed, 6 total
Tests:       22 passed, 22 total
Snapshots:   0 total
Time:        4.255s
Ran all test suites.

Examples

Using the the Vue Weather App, let's look at some examples for testing Vue components.

Example 1 - Unit Test Introduction

Let's jump right into an example of a unit test file in Vue! The first unit test file is found in tests/unit/Header.spec.js and it tests the Header component:

import { shallowMount } from '@vue/test-utils'
import Header from '@/components/Header.vue'


describe('Header.vue Test', () => {
  it('renders message when component is created', () => {
    // render the component
    const wrapper = shallowMount(Header, {
      propsData: {
        title: 'Vue Project'
      }
    })

    // check the name of the component
    expect(wrapper.name()).toMatch('Header')

    // check that the title is rendered
    expect(wrapper.text()).toMatch('Vue Project')
  })
})

Mounting

The first line in this file imports a function called shallowMount from the Vue Test Utils library. The idea of 'mounting' means the loading of an individual component to be able to test it. There are two methods for this in Vue Test Utils:

  • shallowMount() - creates a wrapper for the Vue component, but with stubbed child components
  • mount() - creates a wrapper for the Vue component, including mounting any child components

Since our focus is on testing an individual component (the Header component), we'll use shallowMount().

shallowMount() is better for testing an individual component in isolation, as child components are stubbed out. This is the ideal situation for unit testing.

Additionally, using shallowMount() for testing a component with a lot of child components can improve the execution time of the unit test as there is no cost (in terms of time) for rendering the child components.

mount() is useful when you want to include testing the behavior of the child components.

The second line imports the Vue component that is going to be tested, Header.vue.

Describe Blocks

After the import statements, there is a describe block which defines a unit test suite.

Within a unit test file, there can be multiple describe blocks that define different unit test suites. Similarly, each describe block can contain multiple unit tests, where each unit test is defined by an it block.

I think about this distinction as:

  • describe block - unit test suite
  • it block - individual unit test function

What's really nice about unit testing in Vue is that there are a number of built-in encouragements for adding comments. For example, the describe syntax requires that the first argument be the name of the test suite. When testing Vue components, it's easiest to include the name of the Vue component here.

For each it block, the first argument is a description of the test function, which should be a short description of what this specific test is doing. In the example above, the it block tests that the component 'renders message when component is created'.

Expects

As far as the actual unit testing, the first step is to mount the Vue component so that it can be tested:

// render the component
const wrapper = shallowMount(Header, {
  propsData: {
    title: 'Vue Project'
  }
})

The shallowMount function returns a wrapper object, which contains the mounted component and the methods to test that component. The wrapper object allows us to test all aspects of the HTML generated by the Vue component and all properties (such as data) of the Vue component.

Additionally, the props, passed in to the Header component, are passed in as the second argument to shallowMount().

The actual checks performed in the unit test are:

// check the name of the component
expect(wrapper.name()).toMatch('Header')

// check that the title is rendered
expect(wrapper.text()).toMatch('Vue Project')

These lines perform the following checks on wrapper to test the Vue component:

  1. Check if the name of the component is Header
  2. Check if the title generated by the component is 'Vue Project'

Both of these checks compare strings, so the recommended test function to use is toMatch().

Jest Helpers

While the checks in the unit test file for the Header component are only checking string values, there are lots of options available from Jest to perform checks:

  • Booleans:
    • toBeTruthy() - checks that a variable/statement is true
    • toBeFalsy() - checks that a variable/statement is false
  • Defined:
    • toBeNull() - checks if a variable matches only null
    • toBeUndefined() - checks if a variable is not defined
    • toBeDefined() - check if a variable is defined
  • Numbers:
    • toBeGreaterThan() - checks if a number is greater than the value specified
    • toBeGreaterThanOrEqual() - checks if a number is greater than or equal to the value specified
    • toBeLessThan() - checks if a number is less than the value specified
    • toBeLessThanOrEqual() - checks if a number is less than or equal to the value specified
    • toBe() and toEqual() - checks if a number is the same as the value specified (these functions are equivalent for numbers)
    • toBeCloseTo() - checks if a number is equal to the value specified within a small tolerance (useful for floating-point numbers)
  • Strings:
    • toMatch() - checks if a string is equal to the value specified (Regex can be used as the value specified!)
  • Arrays:
    • toContain() - checks if an array contains the specified value

Additionally, the not qualifier can be used with most of these checks:

expect(wrapper.name()).not.toMatch('Header')

Example 2 - Testing Initial Conditions

This example shows how to test the initial conditions (or state) of the Vue Weather component.

Here's an outline of the unit test file (defined in tests/unit/Weather.spec.js):

import { shallowMount } from '@vue/test-utils'
import Weather from '@/components/Weather.vue'


describe('Weather.vue Implementation Test', () => {
  let wrapper = null

  // SETUP - run before to each unit test
  beforeEach(() => {
    // render the component
    wrapper = shallowMount(Weather, {
      propsData: {
        city: '',
        weatherSummary: '',
        weatherDescription: '',
        currentTemperature: 0.0,
        lowTemperature: 0.0,
        highTemperature: 0.0
      }
    })
  })

  // TEARDOWN - run after to each unit test
  afterEach(() => {
    wrapper.destroy()
  })

  it('initializes with correct elements', () => {
    ...
  })

  it('processes valid props data', () => {
    ...
  })

  it('emits a custom event when clearWeather() is called', () => {
    ...
  })
})

The unit test file for the Weather component utilizes the shallowMount() function for rendering the Weather component, as this component is tested as an individual component in isolation.

BeforEach and AfterEach Blocks

Within the unit test suite (defined within the describe block), there are two new functions defined:

  • beforeEach() - called before the execution of each unit test within this unit test suite
  • afterEach() - called after the execution of each unit test within this unit test suite

The beforeEach() function is used to set a consistent state before running each unit test. This concept is very important to make sure that the order of running the unit tests does not impact the results of the unit tests as a whole.

In this example, the beforeEach() function renders the component with a default set of prop data:

// SETUP - run before to each unit test
beforeEach(() => {
  // render the component
  wrapper = shallowMount(Weather, {
    propsData: {
      city: '',
      weatherSummary: '',
      weatherDescription: '',
      currentTemperature: 0.0,
      lowTemperature: 0.0,
      highTemperature: 0.0
    }
  })
})

The afterEach() function is used to clean-up any processing performed during the unit test.

In this example, the afterEach() function destroys the wrapper used during the unit test, so that the wrapper can be re-initialized for the next unit test in the beforeEach():

// TEARDOWN - run after to each unit test
afterEach(() => {
  wrapper.destroy()
})

If you want to run code that is executed before or after the overall unit test suite is run, you can use:

beforeAll(() => {
  /* Runs before all tests */
})
afterAll(() => {
  /* Runs after all tests */
})

Expects

The first unit test for the Weather component checks the initial conditions of the Weather component:

it('initializes with correct elements', () => {
  // check the name of the component
  expect(wrapper.name()).toMatch('Weather')

  // check that the heading text is rendered
  expect(wrapper.findAll('h2').length).toEqual(2)
  expect(wrapper.findAll('h2').at(0).text()).toMatch('Weather Summary')
  expect(wrapper.findAll('h2').at(1).text()).toMatch('Temperatures')

  // check that 6 fields of data for the temperature are displayed
  expect(wrapper.findAll('p').length).toEqual(6)
  expect(wrapper.findAll('p').at(0).text()).toMatch('City:')
  expect(wrapper.findAll('p').at(1).text()).toMatch('Summary:')
  expect(wrapper.findAll('p').at(2).text()).toMatch('Details:')
  expect(wrapper.findAll('p').at(3).text()).toMatch('Current: 0° F')
  expect(wrapper.findAll('p').at(4).text()).toMatch('High (Today): 0° F')
  expect(wrapper.findAll('p').at(5).text()).toMatch('Low (Today): 0° F')
})

Checks:

  1. The first expect checks that the name of the component matches the name, Weather, that's defined in the Weather component.
  2. The second section of expects check that the two headers (defined as h2 elements) are as expected.
  3. The third section of expects check that the six data fields (defined as p elements) are as expected.

Example 3 - Testing Props

The second unit test for the Weather component checks that valid data passed in as prop data is handled correctly by the Weather component:

it('processes valid props data', () => {
  // Update the props passed in to the Weather component
  wrapper.setProps({
    city: 'Chicago',
    weatherSummary: 'Cloudy',
    weatherDescription: 'Cloudy with a chance of rain',
    currentTemperature: 45.1,
    lowTemperature: 42.0,
    highTemperature: 47.7
  })

  // check that the prop data is stored as expected within the component
  expect(wrapper.vm.city).toMatch('Chicago')
  expect(wrapper.vm.weatherSummary).toMatch('Cloudy')
  expect(wrapper.vm.weatherDescription).toMatch('Cloudy with a chance of rain')
  expect(wrapper.vm.currentTemperature).toEqual(45.1)
  expect(wrapper.vm.lowTemperature).toBeCloseTo(42.0)
  expect(wrapper.vm.highTemperature).toBe(47.7)

  // check that 6 fields of data for the temperature are displayed
  expect(wrapper.findAll('p').length).toEqual(6)
  expect(wrapper.findAll('p').at(0).text()).toMatch('City: Chicago')
  expect(wrapper.findAll('p').at(1).text()).toMatch('Summary: Cloudy')
  expect(wrapper.findAll('p').at(2).text()).toMatch('Details: Cloudy with a chance of rain')
  expect(wrapper.findAll('p').at(3).text()).toMatch('Current: 45.1° F')
  expect(wrapper.findAll('p').at(4).text()).toMatch('High (Today): 47.7° F')
  expect(wrapper.findAll('p').at(5).text()).toMatch('Low (Today): 42° F')
})

Since the beforeEach() function provides a default set of prop data, we need to override the prop data using the setProps() function.

Checks:

  1. With the props data updated, we can check that the prop data was properly stored within the Weather component by checking the data elements (using wrapper.vm).
  2. The second set of expects check that the prop data is used to set the six data fields (defined as p elements) are as expected.

Example 4 - Testing User Input (Click Event)

The third unit test for the Weather component checks that the clear-weather-data event is emitted by the Weather component when the user clicks on the 'Clear Weather Data' button:

  it('emits a custom event when the Clear Weather Data button is clicked', () => {
    // trigger an event when the 'Clear Weather Data' button is clicked
    wrapper.findAll('button').at(0).trigger('click')

    // check that 1 occurrence of the event has been emitted
    expect(wrapper.emitted('clear-weather-data')).toBeTruthy()
    expect(wrapper.emitted('clear-weather-data').length).toBe(1)
  })

In order to trigger a click event, the button element must be found in the wrapper and then the trigger function is called to trigger the click event.

Once the button is clicked, the unit test checks that only one custom event (with the name of clear-weather-data) is emitted.

Mocking Examples

Within the App component, when a user searches for the weather for a city, an HTTP GET call is made to Open Weather to retrieve the data via a third-party library called Axios:

// GET request for user data
axios.get('http://api.openweathermap.org/data/2.5/weather?q=' + inputCity + '&units=imperial&APPID=' + this.openweathermapApiKey)
  .then((response) => {
    // handle success
    console.log(response)

    this.weatherData.city = response.data.name
    this.weatherData.weatherSummary = response.data.weather[0].main
    this.weatherData.weatherDescription = response.data.weather[0].description
    this.weatherData.currentTemperature = response.data.main.temp
    this.weatherData.lowTemperature = response.data.main.temp_min
    this.weatherData.highTemperature = response.data.main.temp_max
    this.validWeatherData = true
  })
  .catch((error) => {
    // handle error
    this.messageType = 'Error'
    this.messageToDisplay = 'ERROR! Unable to retrieve weather data for ' + inputCity + '!'
    console.log(error.message)
    this.resetData()
  })
  .finally((response) => {
    // always executed
    console.log('HTTP GET Finished!')
  })

When thinking about how to test making the HTTP GET calls, there are two scenarios that come to mind, each testing the side effect of the actual API call:

  • HTTP GET response succeeds (happy path)
  • HTTP GET response fails (exception path)

When testing code that utilizes an external API, it's often easier to not actually make the call to begin with by replacing the call with a mock. There are pros and cons to this approach, though.

Pros:

  1. The tests won't be dependent on a network request
  2. They won't break if the API goes down
  3. They will run much faster

Cons:

  1. You'll need to update the tests whenever the API schema changes
  2. It's difficult to keep up with API changes in a microservice architecture
  3. Mocking is a difficult concept to grasp and they can add a lot of clutter to your test suites

At some point you should check the full integration to ensure the shape of the API response hasn't changed.

Since this article is focused on unit testing, we're going to mock the Axios library.

Mocking provides a means to mimic the expected behavior of a software module. Although mocking can be used in production code (very dangerous!), it's typically used during development and testing.

Loading data from an external API takes time. While it typically takes less than one or two seconds to load the data from Open Weather in this app, other external APIs can be more time consuming. Additionally, we want a way to easily check if the HTTP GET request fails. So, we'll add mocks to specify how the GET request would respond.

Example 5 - Testing Asynchronous Code (Successful Case)

The unit tests for the App component are located in the tests/unit/App.spec.js file.

To get started, we need to import the Axios library:

import { shallowMount } from '@vue/test-utils'
import Content from '@/components/App.vue'
import axios from 'axios'

However, we don't want to use the actual Axios library as is done in the source code (App.vue). Instead, we want to create a mock of the Axios library so we don't actually call the external API:

// Mock the axios library
jest.mock('axios');

This line tells Jest (the framework responsible for executing our unit tests) that we want to mock the Axios library.

In the unit test file (tests/unit/Content.spec.js), for the Content component, we're:

  1. Simulating a successful HTTP GET request
  2. Simulating a failed HTTP GET request

To start, let's handle the nominal situation where the HTTP GET request is successful.

describe('App.vue Test with Successful HTTP GET', () => {
  let wrapper = null

  beforeEach(() => {
    const responseGet = { data:
      {
        name: 'Chicago',
        weather: [
          {
            main: 'Cloudy',
            description: 'Cloudy with a chance of rain'
          }
        ],
        main: {
          temp: 56.3,
          temp_min: 53.8,
          temp_max: 58.6
        }
      }
    }

    // Set the mock call to GET to return a successful GET response
    axios.get.mockResolvedValue(responseGet)

    // render the component
    wrapper = shallowMount(App)
  })

  afterEach(() => {
    jest.resetModules()
    jest.clearAllMocks()
  })

  ...
})

BeforeEach and AfterEach Blocks

In the beforeEach() function, we set the response that should occur when axios.get() is called. The response is the pre-canned weather data that looks similar to what we get from Open Weather, if we actually made the request. The key line in this section is:

// Set the response from the GET call to axios
axios.get.mockResolvedValue(response_get);

This line is the key as it lets Jest know that it should return the response_get array when axios.get() is called in the Content component instead of actually making an HTTP GET call using Axios.

The mockResolvedValue() function provided by Jest is actually a shorthand syntax for:

jest.fn().mockImplementation(() => Promise.resolve(value));

The next section of the unit test suite defines the afterEach() function:

afterEach(() => {
  jest.resetModules();
  jest.clearAllMocks();
});

The afterEach() function, which is called after each unit test executes, clears any mocks that were created during the unit test execution. This approach is a good practice to clean up any mocks after running a test, so that any subsequent tests start from a known state.

Expects

Now we can define the unit test:

it('does load the weather data when a successful HTTP GET occurs', () => {
  wrapper.vm.searchCity('Chicago')

  expect(axios.get).toHaveBeenCalledTimes(1)
  expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))

  wrapper.vm.$nextTick().then(function () {
    // check that the user data is properly set
    expect(wrapper.vm.weatherData.city).toMatch('Chicago')
    expect(wrapper.vm.weatherData.weatherSummary).toMatch('Cloudy')
    expect(wrapper.vm.weatherData.weatherDescription).toMatch('Cloudy with a chance of rain')
    expect(wrapper.vm.weatherData.currentTemperature).toEqual(56.3)
    expect(wrapper.vm.weatherData.lowTemperature).toEqual(53.8)
    expect(wrapper.vm.weatherData.highTemperature).toEqual(58.6)
    expect(wrapper.vm.validWeatherData).toBe(true)
  })
})

Since we already went through the steps of defining the mock and rendering the component (via shallowMount()), this unit test can focus on performing checks.

The unit test starts by calling the searchCity() function:

wrapper.vm.searchCity('Chicago')

We check that axios.get() was called only once and that the HTTP GET call included the correct city name:

expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))

To be very thorough, the weather data also checks for the instance of the App component rendered in this unit test to make sure it matches the pre-canned data returned from the mock of axios.get():

wrapper.vm.$nextTick().then(function () {
  // check that the user data is properly set
  expect(wrapper.vm.weatherData.city).toMatch('Chicago')
  expect(wrapper.vm.weatherData.weatherSummary).toMatch('Cloudy')
  expect(wrapper.vm.weatherData.weatherDescription).toMatch('Cloudy with a chance of rain')
  expect(wrapper.vm.weatherData.currentTemperature).toEqual(56.3)
  expect(wrapper.vm.weatherData.lowTemperature).toEqual(53.8)
  expect(wrapper.vm.weatherData.highTemperature).toEqual(58.6)
  expect(wrapper.vm.validWeatherData).toBe(true)
})

What is Vue.nextTick()?

The set of checks above are not as straightforward, as they require calling the nextTick() function.

The unit test framework that we're using for testing the Vue components is Vue Test Utils. This framework makes a simplification to support easier testing by applying DOM updates synchronously.

However, when working with an external API with the Axios library, we're actually dealing with asynchronous behavior (callbacks/promises).

In order to make sure that the DOM is updated after the callback for a successful HTTP GET call, we need to utilize the nextTick() function to advance the event loop so that the updates in the 'Success' block of our HTTP POST call are applied to the DOM.

If you change the above code to not utilize nextTick(), you'll see that these checks will fail.

Example 6 - Testing Asynchronous Code (Failure Case)

While it's nice to test when things go as expected, it's also important to check how our software reacts to negative conditions. With that in mind, let's create a second unit test suite for checking a failed HTTP GET request.

There is no problem with having multiple unit test suites within a single unit test file (.spec.js).

Since the mocks are created in the beforeEach() function, there needs to be separate unit test suites with different beforeEach() implementations for both successful and failed HTTP GET responses.

BeforeEach and AfterEach Blocks

The beforeEach() function in this unit test suite is quite different:

beforeEach(() => {
  // Set the mock call to GET to return a failed GET request
  axios.get.mockRejectedValue(new Error('BAD REQUEST'))

  // Render the component
  wrapper = shallowMount(App)
})

Instead of setting a response to return from the axios.get() call, we are now returning a failed Promise object with the response of 'BAD REQUEST'.

The afterEach() function for this unit test suite is identical to the other unit test suite:

afterEach(() => {
  jest.resetModules();
  jest.clearAllMocks();
});

Expects

Here's the unit test function for testing a failed HTTP GET request:

it('does not load the weather data when a failed HTTP GET occurs', () => {
  wrapper.vm.searchCity('Chicago')

  expect(axios.get).toHaveBeenCalledTimes(1)
  expect(axios.get).toBeCalledWith(expect.stringMatching(/Chicago/))

  wrapper.vm.$nextTick().then(function () {
    // Check that there is no user data loaded when the GET request fails
    expect(wrapper.vm.weatherData.city).toMatch(/^$/)
    expect(wrapper.vm.weatherData.weatherSummary).toMatch(/^$/)
    expect(wrapper.vm.weatherData.weatherDescription).toMatch(/^$/)
    expect(wrapper.vm.weatherData.currentTemperature).toEqual(0)
    expect(wrapper.vm.weatherData.lowTemperature).toEqual(0)
    expect(wrapper.vm.weatherData.highTemperature).toEqual(0)
    expect(wrapper.vm.validWeatherData).toBe(false)

    // check that the banner message indicates failure
    expect(wrapper.vm.messageToDisplay).toMatch('ERROR! Unable to retrieve weather data for Chicago!')
    expect(wrapper.vm.messageType).toMatch('Error')

    expect(global.console.log).toHaveBeenCalledWith('BAD REQUEST');
  })
})

Just like in the previous unit test suite, we check that only one instance of axios.get() is called and then there is a check that no weather data was loaded into the App component.

Code Coverage

When developing unit tests, it can be nice to get an understanding of how much of the source code is actually tested. This concept is known as code coverage.

I need to be very clear that having a set of unit tests that covers 100% of the source code is by no means an indicator that the code is properly tested.

This metric means that there are a lot of unit tests and a lot of effort has been put into developing the unit tests. The quality of the unit tests still needs to be checked by code inspection.

The other extreme where this is a minimal set (or none!) of unit tests is a very bad indicator.

The configuration of Jest (the unit test runner) can be found in jest.config.js.

Depending on how you configured your Vue app when it was initially created, the Jest configuration can either be in jest.config.js ('In dedicated config files' was the selected configuration option) or in package.json ('In package.json' was the selected configuration option).

In order to config Jest to generate the code coverage of our unit tests, we need to append the following configuration items to this file:

collectCoverage: true,
collectCoverageFrom: [
  "src/**/*.{js,vue}",
  "!**/node_modules/**"
],
coverageReporters: [
  "html",
  "text-summary"
]

These configuration items tell Jest that we want code coverage generated (collectCoverage), where to generate the coverage data from (collectCoverageFrom), and how to report the output (coverageReporters).

With this additional configuration of Jest, let's re-run the unit tests in the Vue CLI:

$ npm run test:unit

> [email protected] test:unit vue-weather-app
> vue-cli-service test:unit

 PASS  tests/unit/App.spec.js
 PASS  tests/unit/Search.spec.js
 PASS  tests/unit/Weather.spec.js
 PASS  tests/unit/Banner.spec.js
 PASS  tests/unit/Header.spec.js
 PASS  tests/unit/Footer.spec.js

=============================== Coverage summary ===============================
Statements   : 89.36% ( 42/47 )
Branches     : 90% ( 9/10 )
Functions    : 93.75% ( 15/16 )
Lines        : 89.36% ( 42/47 )
================================================================================

Test Suites: 6 passed, 6 total
Tests:       19 passed, 19 total
Snapshots:   0 total
Time:        6.077s
Ran all test suites.

Unit Test Structure

After going through a number of different unit test files, I recommend the following structure for the unit test file for a Vue component:

import { shallowMount } from '@vue/test-utils'
import App from '@/App.vue'  // Import Vue component to test
import axios from 'axios'    // Import libraries to mock

// Mock the axios library - Only needed if mocking a library
jest.mock('axios.')


describe('Tests for the ... Component', () => {
  let wrapper = null

  beforeEach(() => {
    // set any initial data and create the mocks of libraries

    // render the component
    wrapper = shallowMount(App)
  })

  afterEach(() => {
    jest.resetModules()
    jest.clearAllMocks()  // Only needed if mocking a library
  })

  it('check the initial conditions when the component is rendered', () => {
    // check the name of the component
    expect(wrapper.name()).toMatch('...')

  })

  it('check successful events', () => {
  })

  ...
})

Key items:

  • Each unit test suite has a related set of unit tests
  • Utilize the beforeEach() and afterEach() functions to create independent unit test functions
  • Create mocks of any libraries used within the Vue component that's tested
  • Render the components in the beforeEach() function and update prop data within the unit test functions
  • Utilize shallowMount() over mount() to focus on testing individual components

Conclusion

This article provides a guide for unit testing Vue components, focusing on:

  1. Why you should write unit tests
  2. What you should (and should not) unit test
  3. How to write unit tests

Put simply, when considering what to test, focus on testing the inputs and outputs (actual results), not the underlying business logic (how the results are produced). With this in mind, take a minute or two to review the examples again, taking note of the inputs and outputs tested.

Again, if you're interested in learning more about Vue, check out my course: Learn Vue by Building and Deploying a CRUD App.

Patrick Kennedy

Patrick Kennedy

Patrick is a software engineer from the San Francisco Bay Area with experience in C++, Python, and JavaScript. His favorite areas of teaching are Vue and Flask. In his free time, he enjoys spending time with his family and cooking.

Share this tutorial

Featured Course

Learn Vue by Building and Deploying a CRUD App

This course is focused on teaching the fundamentals of Vue by building and testing a web application using Test-Driven Development (TDD).

Featured Course

Learn Vue by Building and Deploying a CRUD App

This course is focused on teaching the fundamentals of Vue by building and testing a web application using Test-Driven Development (TDD).