Unit testing for Vue.js and TypeScript

How to set up Karma test runner to unit test Vue.js components written with TypeScript preprocessor

Hajime Yamasaki Vukelic
8 min readApr 22, 2017

In one of the previous posts, I’ve written about how to set Webpack up for .vue files written with TypeScript. In that post, I haven’t covered unit testing. Since programmers are going to be using different solutions for that, I thought, it would probably be better to cover testing in a separate article.

This is that article.

UPDATE: Since writing this article, I have switched to using render functions and Jest. Check out this article for more information.

Why Karma?

Karma is pretty much the de facto standard test runner in the Vue.js community. It is easy to set up, it works reasonably well, and it can run tests in an actual browser as needed.

I’ve also looked into Facebook Jest, but it was not worth the effort in this case. Even though some have had success with it, and I generally like Jest very much, the effort to get it working with TypeScript was simply above the threshold which I consider worthwhile. (It turns out that sacrificing .vue files reduces the effort from “not worthwhile” to “negligible”. More on that here.)

In this article, I will describe a simple and effective set-up that will let you write and execute unit tests with Karma and Jasmine, and use PhantomJS as the target browser. With a few lines of changes, you can later add support for running in real browsers such as Chrome, Firefox, and similar.

Configuring the project

I will assume that you’ve got things set up the same way I did in my Vue.js + TypeScript article.

The gist of it is that we’ll compile all our test code using Webpack itself, to avoid any inconsistencies between our production code and our tests, and also avoid duplicating the tooling that is in charge of transpiling our code.

We will first need to install Karma and its dependencies.

npm i -D karma \
karma-jasmine \
jasmine-core \
karma-phantomjs-launcher \
karma-sourcemap-loader \
karma-webpack

In addition to test runner depdendencies, we will also install type definitions for Jasmine, so that TypeScript compiler does not complain about its global variables.

npm i -D @types/jasmine

Karma needs a configuration file of its own. This is usually generated using karma init command, but we’ll write one manually (erm, I mean, copy and paste) since the Karma’s command line tool is just not flexible enough to account for the options we need.

// Karma configuration
var webpackConfig = require('./webpack.config.js');
module.exports = function (config) {
config.set({
// Paths
basePath: '',
exclude: [],
files: [
{pattern: 'src/**/*.test.ts', watch: false},
],

// Module processing
preprocessors: {
// Process all *test* modules with webpack
// (it will handle dependencies)
'src/**/*.test.ts': ['webpack', 'sourcemap'],
},
/* OTHER CONFIGURATION */ // Webpack config
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-only',
},
});
};

All our test-specific configuration options will go into the object passed to config.set() above. I’ve already included webpack-specific configuration, so if you’re familiar with Karma, you can skip the rest of this section.

With this setup, the test files will be right inside the source tree. For example, if we have src/hello/Hello.vue, our test file will be src/hello/Hello.test.ts. You can tweak the pattern property to place your test files somewhere else (e.g., 'tests/**/*-spec.ts'). Putting tests in a single directory has the benefit of being able to get a quick glance at all of our tests, and peruse it as documentation. On the other hand, having tests alongside our tested modules has the benefit of more flexibility when moving things around, and less hassle when setting webpack up.

If you are not familiar with Karma, I’ll give you the other missing sections one by one and explain what they do. You can simply copy and paste the incoming code replacing the comment that says OTHER CONFIGURATION.

For now, save the file into the root of the source tree and name it karma.config.js.

We only target PhantomJS. This is mostly for startup-speed. You can later add support for other environments by installing additional launchers. We won’t bother with that here.

    // Targets
browsers: ['PhantomJS'],

The next section configures the Karma’s console output. The choice of reporters is a matter of taste. In my experience, verbose reporters are pretty to look at initially, but they tend to become less and less useful as the number of tests grow. Plus I generally don’t need too much info about tests that pass.

    // Reporters
reporters: ['dots'],
logLevel: config.LOG_INFO,
colors: true,

We will use Jasmine to execute the tests. Another possibility is to use Mocha. Again, this is pretty much a matter of taste. One of the readers pointed out that Jasmine is outdated, slow, and does not get updated too often. Mocha is a good alternative in that case. You may run into a few TypeScript-related wrinkles, though. I have also tried AVA, but it did not quite work out the way I imagined it would.

    // Test framework configuration
frameworks: ['jasmine'],

Finally, some configuration options related to Karma runner behavior:

    // Runner configuration
port: 9876,
autoWatch: true,
singleRun: true,
concurrency: Infinity,

Note that we have enabled the singleRun flag. This is intentional. We want the tests to quit when done by default in order to make it easier to integrate into CI infrastructure. We can override this flag on the command line when we want to run the tests in watch mode.

Once our configuration file is done, we can add the test script to package.json.

...
"scripts": {
"test": "karma start karma.config.js",
...
},
...

Now we can run the tests using: the following command:

npm test

To run continuously during development:

npm test -- --no-single-run

Hello component

Here is our dummy component that we’ll test.

<template>
<div class="hello">
<p>Hello, {{ name || "World" }}!</p>
<input v-model="name">
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { Component } from "vue-property-decorator";
@Component
export default class Hello extends Vue {
data(): HelloData {
return {
name: "World"
};
}
};
interface HelloData {
name: string;
};
</script>
<style scoped>
.hello {
font-size: 20px;
font-family: sans-serif;
}
</style>

The component shows “Hello, NAME” where “NAME” comes from a property called name, and this property is also bound to an input.

Hello component rendered in a browser

When the text field is edited, the caption above changes:

Component with text box content modified

When the text box is blank, the default name “World” is shown:

Component with text box cleared

We want to test that:

  • the component will render without an exception
  • input is correctly bound to the name property
  • caption will revert to the default when input is cleared

Writing tests

In our source tree, we will create a new file right next to Hello.vue and name it Hello.test.ts.

Before we write anything serious, let’s just write a dummy test to make sure we’ve configured everything correctly:

import Vue from "vue";
import Hello from "./Hello.vue";
describe("Hello.vue", () => {
it("should fail", () => {
expect(true).toBe(false);
});
});

The reason we have also included the imports is to test that Webpack is working as expected.

Now let’s run the tests:

npm test -- --no-single-run

It all seems to be working. We have one expected failure, and our imports seem to be in order.

We’ll replace our dummy test with a real one next:

  it("should render without exception", () => {
const vm = new Vue({
el: document.createElement("div"),
render(h) {
return h(Hello);
},
});
expect(vm.$el.innerHTML).toBe(
'<p data-v-254f5129="">Hello, World!</p> <input data-v-254f5129="">'
);
});

As we save the test file, Karma should rerun the tests for us:

The next test involves firing an event. Since Karma executes the tests in the browser, we can simply use the DOM API to test this type of scenario.

  it("should change the caption when input value changes", (done) => {
const vm = new Vue({
el: document.createElement("div"),
render(h) {
return h(Hello);
},
});
const input = vm.$el.querySelector("input");
input.value = "Foo";
input.dispatchEvent(new Event("input"));
// After events settle.
Vue.nextTick(() => {
expect(vm.$el.innerHTML).toBe(
'<p data-v-254f5129="">Hello, Foo!</p> <input data-v-254f5129="">'
)
done();
})
});

Here we use the Vue.nextTick() function to execute a block of code after Vue.js has updated our component. This is because events are handled asyncrhonously, so we don’t quite know when our component will update.

Good thing about using TypeScript is that it will even warn us about problems with assumptions we make in our test code:

In the above screenshot, it is referring to these lines:

const input = vm.$el.querySelector("input");
input.value = "Foo";
input.dispatchEvent(new Event("input"));

Since querySelector() call can, in theory, return null, TypeScript warns us that we cannot assume it has value and dispatchEvent properties. In reality, we would need to put these behind type guards, but we will ignore these errors here since this article is about testing set-up.

The third test is omitted here for brevity but it’s similar to our second test.

Conclusion

Hopefully, I was able to give you a sane base for your testing configuration, and perhaps demonstrated the benefit of using TypeScript for writing tests in addition to your code.

Although it looks complicated, it is actually quite easy once you’ve done it a few times. Most of the time you will be copying and pasting configuration files anyway. :-)

The code described in this article can be found on GitHub.

If you find this article useful, please don’t neglect to recommend it using the heart icon below. It helps me learn more about what my readers like to read, and come up with more articles like this.

--

--

Hajime Yamasaki Vukelic

Helping build an inclusive and accessible web. Web developer and writer. Sometimes annoying, but mostly just looking to share knowledge.