Tuesday, 31 October, 2017 UTC


Summary

Unit Testing Vue Components
If you’re wise, before you decide to use a framework, you check to make sure you’ll be able to adequately unit test your application if that framework is in place. So, before jumping onto the Vue bandwagon, this question must be answered: can you unit test a Vue application without going through more trouble than it’s worth? Well, let’s take a look at how to test Vue components and you can come to your own conclusion.
Setting Up
There are several options which unit testing libraries/frameworks to use, but if you’re looking for a recommendation, then use the Vue CLI to scaffold out your Vue project, using either the “webpack”, “browserify”, or “pwa” template, and then answer “Y” to the “Setup unit tests with Karma + X?” (X is Jasmine when you use the browserify template and Mocha for the other two templates). Then it will pretty much all be set up for you. Many people would prefer to make their own decisions about what frameworks to use and how they set up the project to do testing. That’s not a bad thing, but sometimes it’s better to let someone/something else make that decision for you rather than spending precious developer time on researching your own choice.
In this case, Karma allows you to run all of your tests in multiple browsers at once or just PhantomJS as the default. It also integrates with webpack so you know your components are being compiled the same way they are when the application is built. This is especially useful when using Single File Components (aka .vue files), which can be tricky to compile otherwise. Mocha and Jasmine are both quite popular testing libraries, so you can’t go wrong with either one.
If, however, you really want to consider all of your options, then Jest may be a good solution. It’s touted as being very fast, has integrated coverage reporting, and the snapshots feature can make testing the final HTML output extremely simple. If you’re using .vue files, then you’ll need a pre-processor to compile them. This sadly doesn’t work well with CSS Modules, but there are workarounds that work to an extent.
Honestly, any framework should work as long as you either use plain JavaScript components or can find a way to compile the .vue files to JavaScript. There are too many options to be able to go into any kind of detail regarding setup, except to say that Vue CLI can do it for you if you’re OK with their choices.
How to Test
For these examples, it’s assumed that you’re using the unit testing setup that Vue CLI scaffolded out for you with the “webpack” template. For the most part, the concepts carry over to the other testing libraries. If you’re using something else, you should hopefully be able to follow along anyway.
To start with, let’s create a relatively simple component that’s pretty useless but will allow us to learn how to test multiple aspects of Vue components. We’ll call it Foo.vue:
<template>
  <div class="foo" @click="giveZero">
    <h1>{{ msg }}</h1>
    <Bar></Bar>
    <p>Some Text</p>
  </div>
</template>

<script>
import Bar from './Bar'

export default {
  name: 'Foo',
  components: { Bar },
  data () {
    return {
      who: 'world',
      updates: 0
    }
  },
  computed: {
    msg () {
      return 'Hello ' + this.who
    }
  },
  watch: {
    who () {
      this.updates++
    }
  },
  methods: {
    giveZero () {
      return 0
    }
  }
}
</script>
Now let’s create our first unit test file for this file, which should be located in the test/unit/specs folder if you’re using the same scaffolding template. We’ll call it Foo.spec.js and just write out the very basic things to set up:
import Vue from 'vue'
import Foo from '@/components/Foo' // @ is configured to be our main src directory

decribe('Foo.vue', () => {
  // Our tests will go here 
}

Static Components

The first and simplest thing to test is the static functions on your components. In this case, we can test Foo.data and Foo.methods.giveZero, which should work without instantiating or mounting the component. Foo.computed.msg and Foo.watch.who both reference this, so they’ll need to be tested on an instance of Foo.
it('should have correct `data`', () => {
  expect(typeof Foo.data).to.equal('function')
  const data = Foo.data()
  expect(data.who).to.equal('world')
  expect(data.updates).to.equal(0)
})
it('should return 0 from `giveZero`', () => {
  expect(typeof Foo.methods.giveZero).to.equal('function')
  expect(Foo.methods.giveZero()).to.equal(0)
})
All of these tests should pass, so let’s take a look at testing a component instance since you can’t get very far simply by testing the static component definition.

Component Instances

To do this we need to use Vue to mount the component, but it won’t be mounted to any element in the DOM; it will just render it in memory. There are two ways to do this:
<span class="hljs-comment">// Mount method 1</span>
<span class="hljs-keyword">const</span> Constructor = Vue.extend(Foo)
<span class="hljs-keyword">const</span> vm1 = <span class="hljs-keyword">new</span> Constructor().$mount()

<span class="hljs-comment">// Mount method 2</span>
<span class="hljs-keyword">const</span> vm2 = <span class="hljs-keyword">new</span> Vue(Foo).$mount()
While the first method is more verbose, it has the advantage of allowing you to pass in options for props. For example:
<span class="hljs-keyword">const</span> Constructor = Vue.extend(Foo)
<span class="hljs-keyword">const</span> vm1 = <span class="hljs-keyword">new</span> Constructor({
  propsData: {
    someProp: <span class="hljs-string">'custom value'</span>
  }
}).$mount()
That’s equivalent to the following code inside a template:
<span class="hljs-tag">&lt;<span class="hljs-title">Foo</span> <span class="hljs-attribute">some-prop</span>=<span class="hljs-value">"custom value"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">Foo</span>&gt;</span>
You’ll likely end up mounting your components quite a bit during your testing, so it may be wise to create a helper function to do it and put it into a module that you can import into all of your tests.
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mount</span><span class="hljs-params">(component, options)</span> </span>{
  <span class="hljs-keyword">const</span> Constructor = Vue.extend(component)
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Constructor(options).$mount()
}
Now let’s write a couple tests to see how to use mount:
it(<span class="hljs-string">'someProp defaults to "default value"'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo)
  expect(foo.someProp).to.equal(<span class="hljs-string">'default value'</span>)
})

it(<span class="hljs-string">'someProp can be set'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  expect(foo.someProp).to.equal(<span class="hljs-string">'custom value'</span>)
})
Note, just like when working inside a component, once an instance is created, all props, data, and computed properties can be accessed directly off the root of the instance. There’s no need to find a computed property under foo.computed.msg or anything like that.
it(<span class="hljs-string">'computed property is correct'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo)
  expect(foo.msg).to.equal(<span class="hljs-string">'Hello world'</span>)
})

Component DOM Rendering

Arguably, the most important thing that a component can do is render the correct DOM, so it’s probably important to test if the DOM was rendered correctly. We can access the DOM through the $el built-in property on our component instance. Through this, we can check anything we want, such as whether or not the correct text was rendered in a certain element. For example:
it(<span class="hljs-string">'render proper DOM'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  expect(foo.$el.querySelector(<span class="hljs-string">'h1'</span>).textContent).to.equal(<span class="hljs-string">'Hello world'</span>)
  expect(foo.$el.querySelector(<span class="hljs-string">'p'</span>).textContent).to.equal(<span class="hljs-string">'custom value'</span>)
})

Reactivity

One of Vue’s greatest strengths is its ability to automatically propagate changes wherever they need to go when one change is made. For example, a computed property will be updated automatically when one of its dependent properties changes. Also, the component will re-render with new data when a change happens. So we need to be able to test the reactive outcomes of changes we make as well.
When testing computed properties, it’s a simple matter of checking to see if the computed property’s value reflects what it should be once a dependency changes:
it(<span class="hljs-string">'computed property updates correctly'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo)
  foo.who = <span class="hljs-string">'universe'</span>
  expect(foo.msg).to.equal(<span class="hljs-string">'Hello universe'</span>)
})
Checking that those updates propagate to the rendered DOM is a bit more trouble, though. If we make a change to foo.who like we just did and then check the DOM, it’ll give us the same DOM output that it had when the foo was initialized, so the following tests will fail:
it(<span class="hljs-string">'render proper DOM on changes'</span>, () =&gt; {
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  foo.who = <span class="hljs-string">'universe'</span>
  foo.someProp = <span class="hljs-string">'really custom value'</span>
  expect(foo.$el.querySelector(<span class="hljs-string">'h1'</span>).textContent).to.equal(<span class="hljs-string">'Hello universe'</span>) <span class="hljs-comment">// FAIL!!</span>
  expect(foo.$el.querySelector(<span class="hljs-string">'p'</span>).textContent).to.equal(<span class="hljs-string">'really custom value'</span>) <span class="hljs-comment">// FAIL!!</span>
})
This happens because Vue wisely renders asynchronously. This prevents it from blocking the JS thread, but it also allows the entire chain of reactive changes to finish taking place before it renders, so it doesn’t end up rendering multiple times. We can use Vue.nextTick and pass it a callback that will run once it finishes the next rendering cycle, which will allow us to test the DOM.
To allow us to test asynchronous functionality, we need to use a done parameter (you can use any name you want but “done” is pretty common) in the callback to it so we can tell our test runner when the test finishes.
it(<span class="hljs-string">'render proper DOM on changes'</span>, (done) =&gt; { <span class="hljs-comment">// &lt;- Add `done` here</span>
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  foo.who = <span class="hljs-string">'universe'</span>
  foo.someProp = <span class="hljs-string">'really custom value'</span>

  Vue.nextTick( () =&gt; {
    expect(foo.$el.querySelector(<span class="hljs-string">'h1'</span>).textContent).to.equal(<span class="hljs-string">'Hello universe'</span>)
    expect(foo.$el.querySelector(<span class="hljs-string">'p'</span>).textContent).to.equal(<span class="hljs-string">'really custom value'</span>)
    done() <span class="hljs-comment">// Call `done` to say we're done</span>
  })
})
Alternatively, you can return a Promise if your test runner supports it (I believe all latest releases of test runners do). And if you’re using Promises, you make it easier to read by using async functions with async and await. First, we’ll need to convert nextTick to use promises though by creating a helper function:
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">nextVueTick</span><span class="hljs-params">()</span> </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Promise((res) =&gt; Vue.nextTick(res))
}
Now we can convert our previous test to look like this:
it(<span class="hljs-string">'render proper DOM on changes'</span>, async () =&gt; { <span class="hljs-comment">// &lt;- remove `done` and add `async`</span>
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  foo.who = <span class="hljs-string">'universe'</span>
  foo.someProp = <span class="hljs-string">'really custom value'</span>

  await nextVueTick() <span class="hljs-comment">// &lt;- use `await` instead of a callback</span>

  expect(foo.$el.querySelector(<span class="hljs-string">'h1'</span>).textContent).to.equal(<span class="hljs-string">'Hello universe'</span>)
  expect(foo.$el.querySelector(<span class="hljs-string">'p'</span>).textContent).to.equal(<span class="hljs-string">'really custom value'</span>)
})
If you’re using Jest or another testing framework that supports snapshots, the simplest way to test the DOM is simply to use foo.$el.outerHTML and compare it to a snapshot. That ensures you’re able to test the entire rendered DOM rather than picking and choosing bits to check.

Spying on Functions

We can also use spies to make sure that certain functions are called:
it(<span class="hljs-string">'call `giveZero` on click'</span>, () =&gt; {
  sinon.spy(Foo.methods, <span class="hljs-string">'giveZero'</span>) <span class="hljs-comment">// &lt;- spy on `giveZero`</span>
  <span class="hljs-keyword">const</span> foo = mount(Foo, { propsData: { someProp: <span class="hljs-string">'custom value'</span> } })
  
  foo.$el.dispatchEvent(<span class="hljs-keyword">new</span> Event(<span class="hljs-string">'click'</span>)) <span class="hljs-comment">// trigger event that will call giveZero</span>
  
  expect(Foo.methods.giveZero.called).to.equal(<span class="hljs-literal">true</span>)
  Foo.methods.giveZero.restore() <span class="hljs-comment">// Remove the spy</span>
})
Note that we create the spy on the component definition rather than on the instance in this case. This is because when the instance is created, the click handler will have a reference to the method that was already defined. If we override it with a spy on the instance, it will never be called because the spy won’t get registered as the click handler. Also, note that we’re checking Foo.methods.giveZero.called rather than foo.giveZero.called. This is because when an instance is created, Vue will wrap giveZero, so foo.giveZero.called is undefined. Calling Foo.methods.giveZero.restore() will return giveZero to its original function and will prevent other tests from using the spy.
We can also use spies to check on watch functions, which are similar to DOM rendering in that they are asynchronous (there are ways to make them synchronous, but generally you won’t be doing that). This means we’ll need to bring back our nextVueTick helper:
it(<span class="hljs-string">'watcher triggered when `who` changes'</span>, async () =&gt; {
  sinon.spy(Foo.watch, <span class="hljs-string">'who'</span>) <span class="hljs-comment">// &lt;- spy on `who` watcher</span>
  <span class="hljs-keyword">const</span> foo = mount(Foo)

  foo.who = <span class="hljs-string">'universe'</span>
  await nextVueTick()

  expect(Foo.watch.who.called).to.equal(<span class="hljs-literal">true</span>)
  expect(foo.updates).to.equal(<span class="hljs-number">1</span>)
  Foo.watch.who.restore() <span class="hljs-comment">// Remove the spy</span>
})
Conclusion
That’s pretty much it. As you can see, just about everything about Vue components is testable without requiring too much finagling, so Vue’s testability should not prevent anyone from making it their choice of web UI framework.

Additional Help

If that’s not simple enough for you, then maybe you should check out one of the following libraries that are designed to simplify some of the aspects around testing Vue components.
Avoriaz: This library simplifies mounting, along with being able to pass more options in during mounting. It wraps components to help make DOM traversal and event triggering simpler. Avoriaz can also do shallow rendering so the rendered DOM isn’t dependent on child components. With this in place, you won’t need to update tests for parent components when the child component is changed. Finally, it offers a method to synchronously re-render, but asynchronous watchers still need to use Vue.nextTick.
vue-test-utils: This library is officially supported by the Vue development team, but currently it’s only in beta. It took heavy inspiration from Avoriaz, but is looking to expand its capabilities as well as make improvements to the API.
The post Unit Testing Vue Components appeared first on appendTo.