Unit testing with Vue.js (using vue-test-utils, mocha-webpack and the vue-cli)

Posted by: Daniel Jimenez Garcia , on 5/30/2018, in Category HTML5 & JavaScript
Views: 7502
Abstract: Learn to write tests using the awesome vue-test-utils together with mocha-webpack. Also see how the vue-cli facilitates writing unit tests from the very beginning.

I have been using Vue.js as my default frontend framework since last year.

I have been positively surprised by this framework which strikes a great balance between ease of use, simplicity, performance, and extensibility while leveraging modern frontend technologies like webpack, babel or ES6 to name a few.

However, this might intimidate the uninitiated, as a certain understanding of such technologies is needed in addition to the knowledge of the framework itself. This is even worse when it comes to testing, which is an intimidating topic per se.

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering Vue.js, Angular, React, .NET Core, MVC, Azure and more. Subscribe to this magazine for FREE and download all previous, current and upcoming editions.

Now you not only need to learn about Vue.js, the tools/technologies it is built upon as well as testing; but you also need to deal with the specifics of writing tests for Vue in a modern JavaScript environment!

In this article, we will see how easy it is to write tests using the awesome vue-test-utils together with mocha-webpack. We will also see how the vue-cli does a great job at wiring all these technologies, so a new project has everything necessary to write unit tests from the very beginning.

Editorial Note: If you are new to Vue.js, here’s an introduction www.dotnetcurry.com/javascript/1349/introducing-vue-js-tutorial

I hope you will find the article interesting and relevant to your current and future Vue projects! Even if you are not using the vue-cli, you should still find the information relevant once you manually setup mocha-webpack and vue-test-utils.

You can find the companion code on github.

Unit Testing with Vue - Setting up a new project

First of all, make sure you have Node and npm installed on your machine. If you need to, you can download the installer from its official page. Other than that, you will just need your favorite editor and a browser.

With the prerequisites sorted, the first thing we are going to do is to create a project using the vue-cli as it will give us a good starting point with most of what we need already wired out of the box.

That is, with one single command, we will be able to generate a project with Vue, webpack, and mocha-webpack ready to go!

Installing the vue-cli is as simple as running the following command in your favorite shell:

npm install -g @vue/cli

Once you have the cli installed, you can start the process for creating a new project by running its create command:

vue create unit-test-example

You will immediately get prompted with the first option of the creation process, which once completed will create the project inside the specified folder (defaulted from the project name, as in ./unit-test-example in the example above)

Make sure to select “Manually select features” in the first prompt:

create-new-vuejs-project

Figure 1: Creating a new vue project

When you manually select the features, the vue-cli will let you customize many of the tools and technologies that you can use. Following the creation process, make sure you select:

  • Selected features: Router, Vuex, Linter, Unit test
  • Pick a unit testing solution: Mocha

You can combine these with other options but if you prefer using the same selection I made, you can check the following screenshot:

selected-vuejs-project-feature

Figure 2: summary of the selected features for the new project

Once the project is generated, switch to the folder where it has been created. You should now be able to:

- Run the npm test command and see an example unit test passing.

- Run the npm run serve command and see your website on localhost:8080

running-npm-test

Figure 3: running npm test

running-npm-run-serve

Figure 4: running npm run serve

generated-vue-app

Figure 5: the generated app running locally

That’s it, the project is ready for us to start adding more code and writing tests for it! Before we move on, let’s take a deeper look at how things work under the covers when using the vue-cli.

Understanding how the vue-cli works

When you open your project in an editor like VS Code and inspect the generated files, a couple of things will take you by surprise:

- There is no webpack.config.js file(s) anywhere in the project. However, when you run npm run serve, the app is bundled with webpack and served with the webpack-dev-server!

- The dependencies listed in package.json do not include the expected webpack, webpack-web-server, various webpack loaders, and mocha-webpack. Instead you will see the @vue/cli-service and various @vue/cli-plugin-* dependencies.

- The scripts defined in package.json all defer to the vue-cli-service as in:

"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "test": "vue-cli-service test",
  "lint": "vue-cli-service lint"
},

This is all fine because the vue-cli uses a plugin-based architecture in which plugins can define their own dependencies, command and webpack settings!

Certain base commands and settings are part of the “default plugins” and so are available on every project. You can check for yourself if you open the source code of the main Service object of the @vue/vue-cli-service and inspect the list of default plugins which at the moment looks like:

const builtInPlugins = [
  './commands/serve',
  './commands/build',
  './commands/inspect',
  './commands/help',
  // config plugins are order sensitive
  './config/base',
  './config/css',
  './config/dev',
  './config/prod',
  './config/app'
].map(idToPlugin)

Where each of the command files contains the definition of the command itself (i.e., its name and options) and the function invoked when running the command. This way, if you inspect the source code of the serve command you will see this is the one internally invoking the webpack-dev-server:

module.exports = (api, options) => {
  api.registerCommand('serve', {
    description: 'start development server',
    usage: 'vue-cli-service serve [options]',
    options: {
      '--open': `open browser on server start`,
      '--mode': `specify env mode (default: ${defaults.mode})`,
      '--host': `specify host (default: ${defaults.host})`,
      '--port': `specify port (default: ${defaults.port})`,
      '--https': `use https (default: ${defaults.https})`
    }
  }, args => {
    info('Starting development server...')

    // Lots of code omitted here for brevity

    const server = new WebpackDevServer(compiler, /* options omitted */)

In a similar fashion, you can inspect the definition of the base webpack config by opening the files inside the config folder. You will notice the definition of the webpack configuration relies heavily on webpack-merge and webpack-chain. This way optional plugins added to your project can easily extend/override the default webpack configuration!

When an optional plugin is installed to your project like the @vue/cli-plugin-unit-mocha, this can use the same API to define additional commands (in this case the test command) and additional webpack configuration. Again, this can easily be seen by inspecting its source code:

module.exports = api => {
  api.chainWebpack(webpackConfig => {
    if (process.env.NODE_ENV === 'test') {
      webpackConfig.merge({
        target: 'node',
        devtool: 'inline-cheap-module-source-map',
        externals: // omitted
      })

      // additional settings omitted
    }
  })

  api.registerCommand('test', {
    description: 'run unit tests with mocha-webpack',
    usage: 'vue-cli-service test [options] [...files]',
    options: {
      '--watch, -w': 'run in watch mode',
      // additional options omitted
    },
    details: // omitted
  }, (args, rawArgv) => {
    api.setMode('test')
    // for @vue/babel-preset-app
    process.env.VUE_CLI_BABEL_TARGET_NODE = true
    // start runner
    const execa = require('execa')
    const bin = require.resolve('mocha-webpack/bin/mocha-webpack')
    const argv = [
      // omitted for brevity
    ]

So how is this invoked when you run npm run serve or npm test?

The answer is that the vue-cli-service and the plugins needed for the additional features you selected while creating your project, have all been added as devDependencies to your project:

  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.0.0-beta.6",
    "@vue/cli-plugin-eslint": "^3.0.0-beta.6",
    "@vue/cli-plugin-unit-mocha": "^3.0.0-beta.6",
    "@vue/cli-service": "^3.0.0-beta.6",
    "@vue/eslint-config-airbnb": "^3.0.0-beta.6",
    "@vue/test-utils": "^1.0.0-beta.10",
    "chai": "^4.1.2",
    "lint-staged": "^6.0.0",
    "vue-template-compiler": "^2.5.13"
  },

They will all be found inside your node_modules folder, so the following sequence happens:

1. You run a command like npm test, which is defined as test: “vue-cli-service test” inside the project’s package.json file

2. The @vue/vue-cli-service installed inside node_modules is loaded

3. The @vue/vue-cli-service inspects your dependencies and devDependencies for any additional plugins added to your project (like @vue/cli-plugin-unit-mocha). It then registers any new commands provided by them (so they are available in the CLI) and their hooks which extend the default webpack configuration.

4. The vue-cli-service finds the test command, which in this case is defined by the @vue/cli-plugin-unit-mocha .

5. The test command invokes mocha-webpack and provides the default arguments for it like the test file pattern (defaulted as test/unit/**/*.spec.js) and where to find the webpack configuration.

6. As webpack configuration, it is pointed to a special file provided by the @vue/cli-service (found in @vue/cli-service/webpack.config.js) that returns the result of combining the base webpack configuration with the chained bits from all the different cli plugins, including the @vue/cli-plugin-unit-mocha plugin.

That was quite an in-depth look at the end, but I hope it helped to demystify the vue-cli a bit .

In my view, it is a very powerful tool, but it might feel hard to understand since there is a lot going on under the covers without it being explicitly part of your project.

What if I don’t use vue-cli?

While the vue-cli is a handy and powerful tool, you might not want or be able to use it in your project. In such a case, you will need to manually install and configure all the required dependencies, including:

  • Vue and related libraries like vue-router or vuex
  • webpack and webpack-dev-server
  • the various loaders and webpack plugins like vue-loader, css-loader or uglifyjs-webpack-plugin (just to name a few)

Then you will need to wire the scripts for generating webpack bundles for production and generate/serve them for local development.

This is not a simple process and requires a good (and up to date!) understanding of webpack, Vue and modern JavaScript. As such it is outside the scope of this article which is to show how to write and run Vue unit tests using vue-test-utils and mocha-webpack.

If you already have a Vue project and you just want to add the minimum needed to unit test with the vue-test-utils and mocha-webpack, the official vue-test-utils has a section about it.

Now that we have a project with all the setup needed to test Vue using the vue-test-utils and mocha-webpack, let’s focus on understanding how to write the tests themselves.

Unit Testing basics with vue-test-utils and mocha-webpack

The project generated by the vue-cli includes a simple unit test for one of the Vue components in the project. Let’s use this example test to introduce the basics of mocha, mocha-webpack, and vue-test-utils.

By default, this test is located in $/test/unit/HelloWorld.spec.js and it is unit testing the component $/src/components/HelloWorld.vue. Its contents should look as follows:

import { expect } from 'chai';
import { shallow } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message';
    const wrapper = shallow(HelloWorld, {
      propsData: { msg },
    });
    expect(wrapper.text()).to.include(msg);
  });
});

The first thing I will point is the functions describe and it. When we write unit tests using mocha-webpack, we are still using the mocha testing framework to define our tests. As any other framework, it provides functions to define a test module and each of the individual tests:

- The function describe allows you to define tests grouped into modules. You provide the name for the module and a callback function where you define the individual tests. In this callback, you can also define submodules by adding further calls to describe.

- The function it allows you to define a single test. Again, you provide the name for the test and a callback with the test method itself, where you exercise your code and perform assertions. A test passes if all the assertions succeed.

- When mocha runs these tests, it will print the names of the modules and tests following the same nesting order in which they were defined. Each test will be clearly marked as a success or a failure.

describe('HelloWorld.vue', () => {
  // This is the definition of the HelloWorld.vue test module
  // Define individual tests with the 'it' function
  // Define submodules with nested 'describe' calls

  it('renders props.msg when passed', () => {
    // This is the definition of the test
    // exercise your code and perform assertions
  });
  it('a second test', () => {
    …
  });
  describe('a submodule', () => {
    it('a third test inside the submodule', () => {
      …
    });
  });
});

defining-tests-modules- mocha

Figure 6: defining tests and modules with mocha

This should all sound familiar as it is the same across most test frameworks. Of course, mocha offers other features, some of which you will see during the rest of the article like the usual before/beforeAll/after/afterAll test hooks or support for asynchronous tests.

Let’s now take a look at the assertions, since we said a test will pass if all its assertions succeed. In this simple test, there is one single assertion:

import { expect } from 'chai';
...
describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    ...
    expect(wrapper.text()).to.include(msg);
  });
});

As you can see, the test uses the function expect provided by the assertion library chai. The ‘expect’ flavor of this assertion library provides a BDD (Behavior-driven development) style of assertions which read like natural language. It’s also worth mentioning that mocha doesn’t care which assertion library you use, any library that throws instances of “Error” will be ok.

So far there is nothing new to someone who wrote tests for Node.js applications using mocha and chai. It is time to see where the vue-test-utils and mocha-webpack get involved!

Let’s begin by inspecting the Vue component we are testing, which is defined as single-file Vue component:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    // rest of the template omitted
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String,
  },
};
</script>

<style scoped>
// omitted
</style>

This is a very simple component that receives an input property which is then included in the rendered html, which is exactly what we are testing.

If you remember, at the beginning of the article, I said you would need Node.js installed for running the tests.

That means the tests will run in a Node.js environment, however, Node.js doesn’t know:

  • How to load .vue files. It only knows about JavaScript and json files.
  • How to handle ES6 modules using the import/export syntax. It only knows about the commonJS modules with require/module.exports
  • Even worse, it only knows about relative module imports, and we are using paths from the source code root as in import HelloWorld from '@/components/HelloWorld.vue';

So how are we able to run these tests in Node.js after all?

With mocha-webpack we can process the tests and source code using webpack before the tests are run.

Once webpack has generated plain JavaScript that Node.js can understand (in the form of the output bundle), mocha will be invoked. It is very similar to the fact the browser wouldn’t understand the source code as is written and needs the webpack bundles to be generated in a format the browser can understand.

mocha-vs-mocha-webpack

Figure 7: comparing mocha tests VS mocha-webpack vue tests

This is the reason why the tests need to be run using mocha-webpack instead of directly using mocha and the reason why we can use ES6 modules (import/export) and import .vue files, while still running the tests in a Node.js environment

// This would never work in plain Node
// as Node doesn’t understand import/export nor vue files
import HelloWorld from '@/components/HelloWorld.vue';

The final piece is understanding how we have been able to instantiate the component in our test and perform an assertion over its rendered output.

This is where vue-test-utils fits the puzzle.

The vue-test-utils library provides an API that allows you to mount a component given its definition (for example contained in a vue file) into a virtual DOM rather than a real browser (using jsdom under the covers):

  • This API exposes the shallow method, which allows you to instantiate the HelloWorld.vue component into this virtual DOM. It returns a wrapper that you will use to interact with your component and the virtual DOM it’s being mounted, for example triggering events or inspecting the resulting html.
  • The shallow method also allows you to provide input properties to your component. We will also see through the article how it can be used to inject mocks.
  • There is an alternative to shallow called mount, which is briefly discussed later in the article.

This allows you to write a test that instantiates a component and mounts it into a virtual DOM using the shallow API, and then uses the text() method of the returned wrapper to ensure the html rendered by the component included the expected message:

import { shallow } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';
...
const msg = 'new message';
const wrapper = shallow(HelloWorld, {
  propsData: { msg },
});
expect(wrapper.text()).to.include(msg);

With that, you should now have a decent understanding of everything involved so you can write and run that simple test! If you are still with me, we can start looking at some of the common scenarios you will find when testing Vue components.

Basic testing scenarios

Let’s create a simple component with a counter and a button to increase it, that we can use as the target of the next tests. This is as simple as creating $/src/components/Counter.vue with the following contents:

<template>
  <div>        
    Counter value: <span class="counter-value">{{ value }}</span>
    <br/>
    <button @click="onIncrease">Increase</button>
  </div>
</template>
<script>
export default {
  props: {
    initialValue: {
      type: Number,
      required: false,
      default: 0
    }
  },
  data(){
    return { 
      value: this.initialValue
    };
  },
  computed: {
    stringValue(){
      return this.value.toString();
    }
  },
  methods: {
    onIncrease(){
      this.value += 1;
    }
  }
};
</script>

This is a rather trivial component but enough to showcase common testing scenarios. Let’s begin by creating a new test file $/tests/unit/Counter.spec.js where we create our tests:

import { expect } from 'chai';
import { shallow } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter.vue', () => {
  // Tests to be defined here
});

Testing the rendered output

The example test we analyzed earlier showcased one way of testing its rendered output, by using the text function of the component wrapper. There is an alternative way of testing the rendered output, which is the html function of the component wrapper.

While text returns only the text that would show on a browser, the html includes the full html rendered by the component. The next 2 tests show the difference between these approaches:

describe('when an initial value is provided', () => {
  it('it is rendered by the component', () => {
    const wrapper = shallow(Counter, {
      propsData: { initialValue: 42 },
    });
    expect(wrapper.text()).to.include('Counter value: 42');
  });
});

describe('when omitting the initial value', () => {
  it('the default value 0 is rendered by the component', () => {
    const wrapper = shallow(Counter, {
      propsData: { },
    });
    expect(wrapper.html()).to.include('Counter value: <span class="counter-value">0</span>');
  });
});

Testing the component state

It is also possible to access the data properties of the component instance by getting the instance reference from the component wrapper. This lets you assert directly over the component state (its data properties) instead of the rendered output, letting you decide between the 2 approaches according to your needs.

  • The component state can be seen as an internal implementation detail, and by directly accessing it in your tests, you might be coupling them too much to your implementation.
  • However, testing your component rendered output might make your tests more brittle and dependent on the templates which tend to be modified more frequently.
  • This is a trade-off you will need to solve by yourself.

The component wrapper exposes a vm property with a reference to the actual component instance, where you can directly access its state, computed blocks, methods etc.

This way the previous tests could be written as:

describe('when an initial value is provided', () => {
  it('it is used as the initial counter value', () => {
    const wrapper = shallow(Counter, {
      propsData: { initialValue: 42 },
    });
    const componentInstance = wrapper.vm;
    expect(componentInstance.value).to.be.equal(42);
  });
});

describe('when omitting the initial value', () => {
  it('the initial value is set as 0', () => {
    const wrapper = shallow(Counter, {
      propsData: { },
    });
    const componentInstance = wrapper.vm;
    expect(componentInstance.value).to.be.equal(0);
  });
});

Testing component computed properties

Computed properties can be tested in the same way as the component state, by means of getting a reference to the component instance. This way, testing a computed value is as simple as asserting that the value of the wrapper.vm.computedName is as expected.

However, by their own nature, computed properties depend on either the current component state or other properties. This means you probably want to use the setProps and/or setData functions of the component wrapper to update its state and verify the computed property was reevaluated as expected.

The following tests showcase this, and to make them more interesting they also show how common test initialization can be moved into a beforeEach test hook:

describe('the stringValue computed', () => {
  let wrapper;
  let componentInstance;
  beforeEach(() => {
    wrapper = shallow(Counter, {
      propsData: { initialValue: 42 },
    });
    componentInstance = wrapper.vm;
  });
  it('returns the string representation of the initial value', () => {
    expect(componentInstance.stringValue).to.be.eql("42");
  });
  it('returns the string representation of the updated value', () => {
    wrapper.setData({ value: 99 });
    expect(componentInstance.stringValue).to.be.eql("99");
  });
});

Testing browser event handlers

Our component binds an event handler to the click event of the button that increases the counter. Triggering the click event is straightforward using vue-test-utils, allowing us to write tests to verify the expected behavior.

We just need to locate the html element using the component wrapper, and then use the trigger method with the event name we want to trigger.

As discussed earlier, you could decide to verify how the rendered html changes as a result of the event or how the component state has changed.

The following tests show both approaches:

describe('when the increment button is clicked', () => {
  let wrapper;
  let componentInstance;
  let increaseButton;
  beforeEach(() => {
    wrapper = shallow(Counter, {
      propsData: { initialValue: 42 },
    });
    componentInstance = wrapper.vm;
    increaseButton = wrapper.find('button');
  });
  it('the counter value is increased by one', () => {
    increaseButton.trigger('click');
    expect(componentInstance.value).to.be.equal(43);
  });
  it('the rendered output contains the value increased by one', () => {
    increaseButton.trigger('click');
    expect(wrapper.text()).to.include('Counter value: 43');
  });
});

Testing component methods

Many times, you will define component methods to be used as event handlers for browser events but that’s not always going to be the case. For example, some methods will be used as event handlers of events emitted by nested Vue components, which cannot be triggered when mounting components using the shallow function.

This shouldn’t stop you from testing any component method as you can invoke them directly through the reference to the component instance as in wrapper.vm.methodName(methodArgs).

For example, you could directly invoke the method bound to the increase button with wrapper.vm.onIncrease(). That way you could directly test this method without triggering the button click event as in:

describe('the onIncrease method', () => {
  let wrapper;
  let componentInstance;
  beforeEach(() => {
    wrapper = shallow(Counter, {
      propsData: { initialValue: 42 },
    });
    componentInstance = wrapper.vm;
  });
  it('the counter value is increased by one', () => {
    componentInstance.onIncrease();
    expect(componentInstance.value).to.be.equal(43);
  });
  it('the rendered output contains the value increased by one', () => {
    componentInstance.onIncrease();
    expect(wrapper.text()).to.include('Counter value: 43');
  });
});

As we discussed while testing the component data, by doing this you are getting into the internals of the component and possibly your test knows too much about the internal implementation. Try to trigger the events through the wrapper when possible/practical.

Testing events emitted by the component

It is not uncommon for Vue components to emit events themselves under certain circumstances, that other Vue components can listen and react to. In fact, this is the recommended mechanism for communicating a child component back to its parent.

Let’s update the onIncrease method of our simple component to emit an increased event:

onIncrease(){
  this.value++;
  this.$emit('increased', this.value);
}

The wrapper provided by vue-test-utils also provides a handy way of verifying that the expected events were emitted by the component you are testing.

We can now update the earlier tests for the increase button/method and verify the increased event was emitted as expected:

it('emits an increased event with the new value', () => {
  increaseButton.trigger('click');
  expect(wrapper.emitted().increased.length).to.be.equal(1);
  expect(wrapper.emitted().increased[0]).to.be.eql([43]);
});

Note: you might be wondering what is the difference between to.be.equal() and to.be.eql().

  • With equal you perform a strict equal comparison which requires both values to be of the same type and for Object/Arrays to point to the same instance.
  • With eql you perform a deep equal comparison which works the same for simple value but also succeeds for object/arrays if they are different instances but have the exact same properties.

Instantiating components: mount vs shallow

There is a second way of instantiating components using the vue-test-utils other than the shallow method, which is the mount method.

  • With the shallow method, any child components rendered by the component under test are automatically stubbed. This way the component is tested in isolation and the tests are faster since it doesn’t need to update and render the entire component tree.
  • On the other hand, the mount method will create an instance of every child component, include them in the component tree and include its rendered output in the virtual dom.

You can think of them in terms of unit vs integration tests. The shallow method is perfect for writing unit tests isolating your Vue components, while the mount method is needed when writing integration tests involving several components.

You can read more about this rationale in the vue-test-utils docs.

Mocking Dependencies

Until now we have managed to test our component in isolation by instantiating them with the shallow function and providing any input prop data.

However, we have been testing a decidedly simple component with little interaction with the outside world. Let’s modify it by adding an extra button that will increase the counter according to the result of an HTTP request, which will be sent using the axios library.

First, install the library with the following command:

npm install --save axios

Next update src/main.js to include axios as the $http property inside every component instance. This will allow us to use axios as this.$http inside our components.(And as we will see later, a standard technique across different libraries like the vue-router or vuex)

import axios from 'axios';
Vue.prototype.$http = axios;

Next update the Counter.vue component with a new button and associated method where the counter is increased according to a json response from an HTTP request:

// on the template
<button id="increase-http-btn" @click="onIncreaseFromHttp">
  Increase from Http response
</button>

// on the script
methods: {
  ...
  onIncreaseFromHttp(){
    return this.$http.get('https://jsonplaceholder.typicode.com/users').then(response => {
      this.value += response.data.length;
      this.$emit('increased', this.value);
    });
  }
}

This adds a new challenge to our tests!

In order to test the behavior of the new button, we need to mock the $http property of the component instance so it doesn’t try to send a real HTTP request and instead uses some predefined behavior.

Using sinon to create stubs/spies

Before we move onto injecting mocks for our dependencies, we first need to define them. This is where a library like sinon.js will be of great help as it provides a simple but powerful way of defining mocks for any dependency you need.

  • The first thing you should know when using sinon is that they have split the api for creating stubs and spies. A spy is basically a method that returns nothing but keeps track of every call received.
  • A stub is an extension of a spy. Apart from keeping track of individual calls, it lets you define the behavior for those calls. For example, you can define a stub that always returns a value or a stub that returns a particular value when some specific inputs are provided.

Before we can use sinon, we will need to add it as a devDependency to our project:

npm install --save-dev sinon

Now we can take a look at how to mock the method call this.$http.get('https://jsonplaceholder.typicode.com/users') so it returns a predefined json response and we can then assert the right value was increased. This is as simple as creating a sinon stub defined with the following behavior:

const mockResponse = {
  data: [{a: 'user'}, {another: 'user'}]
};
const mockAxios = {
  get: sinon.stub()
            .withArgs(‘https://jsonplaceholder.typicode.com/users’)
            .returns(Promise.resolve(mockResponse))
}

This way we are simulating the actual HTTP API (which returns an array of users in its json response) when the GET method is invoked with the correct url.

While this is enough to continue with our article, it barely scratches the surface of what sinon can do for you. If you are going to write tests in a JavaScript environment, it is really worth getting used to it.

Injecting mocks into the component instance

We know how to define a mock as either a static object or a more dynamic sinon stub/spy. Now we need to inject these mocks into the Vue component instance when using the shallow/mount methods.

Luckily for us, both methods to create a component instance accept a mocks object in their arguments that will be injected into the component. Thus, providing a sinon stub for the this.$http component property is as simple as providing the mock in the shallow/mount call:

wrapper = shallow(Counter, {
  propsData: { initialValue: 42 },
  mocks: {
    $http: mockAxios
  }
});

Now we have everything we need to verify the behavior of the new button which increases the counter by the number of objects in the HTTP response array:

it('the counter value is increased by the number of items in the response', (done) => {
  increaseHttpButton.trigger('click');
  setImmediate(() => {
    expect(componentInstance.value).to.be.equal(44);
    done();
  });
});

The main change with the previous test is that now our test is asynchronous as it depends on a Promise being resolved (even if we mocked this.$http.get it still returns a Promise). That’s why our test method accepts a done parameter and wraps the execution of the assertion in a call to setImmediate. This way the promise can be resolved before the body of the setImmediate is executed and our assertions run.

Mocha has a nice support for asynchronous tests we could tap into if we were testing the new onIncreaseFromHttp method directly, as we could chain our assertions directly into the promise chain:

it('increases the value by the number of items in the response', () => {
  return componentInstance.onIncreaseFromHttp().then(() => {
    expect(componentInstance.value).to.be.equal(44);
  });
});

You can read more about the support for asynchronous testd in mocha in its docs.

Mocking vue-router

When using the vue-router, your component gets injected with two properties:

  • The current route is available as this.$route
  • The router object that lets you use the vue-route api to navigate or work with the history is available as this.$router

This means we can simply create a mock for them and provide them as mocks in the shallow/mount call. Imagine your component uses the current route in some method or computed property:

computed: {
  currentRoute(){
    return this.$route.name;
  }
}

We can just create a mock route object and inject it on the call to create the instance:

describe('the currentRoute property', () => {
  let wrapper;
  let componentInstance;
  beforeEach(() => {
    const mockRoute = { name: 'foo' };
    wrapper = shallow(Counter, {
      mocks: {
        $route: mockRoute
      }
    });
    componentInstance = wrapper.vm;
  });
  it('returns the name of the current route', () => {
    expect(componentInstance.currentRoute).to.be.equal('foo');
  });
});

Now let’s see how we can deal with the router. Let’s suppose a certain action triggers some programmatic navigation in your component:

methods: {
  onNavigate(){
    this.$router.push({name: 'home'});
  },
}

It’s equally straightforward to create a sinon spy (since we just want to track the received calls) and create a test where we verify the expected navigation was invoked:

describe('the onNavigate method', () => {
  let wrapper;
  let componentInstance;
  let mockRouter;
  beforeEach(() => {
    mockRouter = {
      push: sinon.spy()
    };
    wrapper = shallow(Counter, {
      mocks: {
        $router: mockRouter
      }
    });
    componentInstance = wrapper.vm;
  });
  it('navigates to the home page', () => {
    componentInstance.onNavigate();
    sinon.assert.calledWith(mockRouter.push, {name: 'home'});
  });
});

Many other Vue libraries and plugins follow the same idea of injecting methods and properties into the component instance, like vuex which injects the $store property. This means by following this approach, you will be able to mock dependencies on more than one of Vue’s plugins/libraries.

Using babel-plugin-rewire to mock external dependencies

Not every dependency will be a friendly Vue plugin that injects methods/properties into the component instance. Sometimes we will find dependencies which are simply imported into the component code.

For example, imagine you have a component that loads a list of users through an HTTP request, but it is directly importing the axios library:

import axios from 'axios';
...
methods:{
  onLoadUsers(){
    return axios.get('https://jsonplaceholder.typicode.com/users').then(response => {
      this.users = response.data;
    });
  }
}

In this case, we cannot simply pass the mock within the call to the shallow function. Our component is directly loading the library with import axios from 'axios', so we need a different strategy:

  • With the latest versions of the vue-loader (13.0.0 or higher ) the preferred way is using the babel-plugin-rewire.
  • In previous versions, you could also use the inject-loader but this no longer works, so be careful when you read some “old” 2017 tutorials.

As usual, start by installing the library:

npm install --save-dev babel-plugin-rewire

Then, make sure you enable it on your .babelrc file, exclusively for the test environment:

{
  "presets": [
    "@vue/app"
  ],
  "env": {
    "test": {
      "plugins": [ "rewire"]
    }
  }
}

Now the plugin is wired and every time we import a Vue component (or any other file) a function named __Rewire__ lets you replace any dependency loaded through an import statement.

Since our component loads axios as:

import axios from 'axios';

Which means in our test we can replace the library with a mock as in:

mockAxios = {
  get: sinon.stub()
            .withArgs('https://jsonplaceholder.typicode.com/users')
            .returns(Promise.resolve({data: mockUsers}))
};
Users.__Rewire__('axios', mockAxios);

Where the first parameter to the rewire function should match the name of the variable and not the string used with the import statement!

That’s it, you should now be able to mock external dependencies which are directly imported by your components. Such a test could look like this:

let mockAxios;
let mockUsers;
beforeEach(() => {
  mockUsers = [{a: 'user'}, {another: 'user'}];
  mockAxios = {
    get: sinon.stub()
              .withArgs('https://jsonplaceholder.typicode.com/users')
              .returns(Promise.resolve({data: mockUsers}))
  };
  Users.__Rewire__('axios', mockAxios);
});

describe('when the load users button is clicked', () => {
  it('updates the list of users with the HTTP response', (done) => {
    const wrapper = shallow(Users);
    const componentInstance = wrapper.vm;
    wrapper.find('button').trigger('click');

    setImmediate(() => {
      expect(componentInstance.users).to.be.equal(mockUsers);
      done();
    });
  });
});

Conclusion

Writing tests is not easy.

Particularly when you want to write good tests, the ones that give you confidence about your software without slowing you down, locking your design choices, or costing you too much time/money. In fact, it is probably one of the hardest aspects to master about software development.

That’s why it’s worth designing your testing strategy with different types of tests in mind (unit, integration, e2e). In the context of web applications, as more and more functionality and effort are put into the client code, you also need to consider the client code in your testing strategy.

Client-side testing with modern frameworks like Vue gets even more complicated due to the number of tools/libraries involved. The fact that half of this lengthy article is dedicated to explaining how things are setup and work under the covers, is a testament to that.

But as I hope the second half of the article has shown, once the basic setup is in place, writing tests for your Vue components is a surprisingly pleasant and straightforward experience. And one would hope that as we get better tooling like the vue-cli, it becomes easier to get started writing unit tests and more developers find themselves writing them.

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Daniel Jimenez Garcia is a passionate software developer with 10+ years of experience. He started as a Microsoft developer and learned to love C# in general and ASP.NET MVC in particular. In the latter half of his career he worked on a broader set of technologies and platforms while these days he is particularly interested in .Net Core and Node.js. He is always looking for better practices and can be seen answering questions on Stack Overflow.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

FREE .NET MAGAZINES

Free DNC .NET Magazine

Tags

JQUERY COOKBOOK

jQuery CookBook