In late September, Vue.js released the beta of vue-test-utils
. Let's take this opportunity to dive a little deeper and take a look at the official Vue testing solution named vue-test-utils
.
We'll be using Jest for testing. For us at Codeship, it turned out to be an excellent tool that's fast and reliable when it comes to continuously running JavaScript unit tests.
Set Up Jest and Run Your First Spec
Why choose Jest in the first place? There are several reasons that I found to be major selling points for this test runner.
It comes with batteries included
You almost need nothing else to get running with your specs. Jest comes with an assertion library, mocking functionality, and code-coverage reporter baked in. Of course, those tools use popular solutions like Jasmin and Istanbul under the hood.
These are baked in nicely, so there is no extra effort required to get the best of those tools. Mocking functionality is also a feature that works well and will be even extended in the near future by the Jest team.
No browser necessary
As Jest runs on NodeJS, there is no real browser required for running your JavaScript specs. For specs that actually wouldn't require a browser environment at all, Jest allows running those directly on the node CLI for maximum power.
For specs that do need a browser environment, Jest provides jsDOM as a possible environment. The latter is configured as default environment. That's handy when we want to test Vue components.
Mocking is easy
As mentioned above, Jest comes with mocking functionality in place. You’d mock something using the jest.fn()
method.
let mocked = jest.fn()
And the API that the mocked library comes with is quite handy. From mocking return values to checking all params the function may receive, Jest has you covered. Also when it comes to tinkering with timeout functions (setTimeout
, setInterval
) you'll have some helpful tools at hand. Some examples follow later in this post.
Snapshot testing
Snapshots are great. They allow for testing strings against each other. That string can be a rendered piece of HTML, a complex JSON object, or whatever you come up with. A snapshot compares the output against a saved and verified version of that output and shows a git-diff-like output if something is wrong. But I guess you are now as excited to get started with some specs as I am.
So let's get to it. Getting started with Jest is quite straight-forward. The following code examples are assuming yarn is installed, and a package.json is ready for you. (Should you still need a package.json, run yarn init -y
to create one quickly.)
$ yarn add jest
This command will install Jest for your project. Although we could use it right away by running the command yarn jest
, I prefer to have a task called test
for that. So add those lines to your package.json
.
// package.json { ... "scripts": { "test": "jest" } }
This will allow for calling Jest through the yarn test
command.
Before actually writing our specs, the system should be set up to support a modern JavaScript workflow. We’ll do that through babel
, so here’s what needs to be done:
$ yarn add babel babel-jest babel-preset-env
If you’ve not heard of babel-preset-env, it basically allows compiling the JavaScript based on the browsers you plan to support. Get more info here: babel-preset-env
Be sure to update your .babelrc
file accordingly.
{ "presets": [ ["env", { "targets": { "browsers": ["last 2 versions", "safari >= 7"] } }] ] }
Now you’re all set to write some beautiful specs. So let’s get to it.
You can create your specs anywhere in your project. Jest will find everything that’s called *.spec.js
or *.test.js
automatically.
// coolifier.js export default function (str) { return `cool${str.trim()}` }
// coolifier.spec.js import coolifier from 'coolifier' describe('coolifier', () => { test('makes my cat cooler', () => { expect(coolifier('cat')).toBe('coolcat') }) })
If you run Jest now, you should get some nice output telling you the spec passed. Yay. The first spec in Jest done. Now to the really cool stuff.
!Sign up for a free Codeship Account
Testing a Vue Component
At Codeship, we have had Vue in production for quite some time already. Testing was not perfect in the beginning. We verified a lot of functionality through rspecs. Are certain elements in place? Will a click trigger following action? And so on.
Basically, we moved "unit testing" into the context of "acceptance testing" what's not ideal. But the technical setup we had before wasn't reliable enough when working with async code, promises, and so on, unless we were throwing multiple libraries into the mix. That made the code ugly, hard to maintain, and still not more trustworthy.
With Jest, that became easier, faster, and more reliable. So why not move some "acceptance testing" into the context of "unit testing" and benefit from the tools we have now?
// Component.js export default { name: 'my-status', props: { status: { type: String, default: 'loading' } }, template: ` <div :class="localStatus" @click="notify"> {{ statusMessage }} </div> `, data() { return { localStatus: this.status } }, methods: { setStatus(newStatus) { this.localStatus = newStatus }, notify() { alert(`Your current status: ${this.localStatus}`) } }, computed: { statusMessage() { if(this.localStatus == 'loading') return 'Loading status' else return `Current status: ${this.localStatus.toUpperCase()}` } }, mounted() { setTimeout( () => { this.setStatus('good') }, 1000) } }
Here we have a status component that mainly displays whatever status it receives. It would maybe interact with a server to update the status in real life, but for now, we will just use a setTimeout
function we added to the mounted
hook. Also, this component has a lot of interesting things we could test. Let's start easy and look at how to check for the passed-in property.
Using the vue-test-utils
First, we need to make use of the vue-test-utils
, so add them to your project:
$ yarn add vue-test-utils@1.0.0-beta
Second, create a spec file:
// component.spec.js import { mount } from 'vue-test-utils' import Component from './component' test('our component comes with a default value for status', () => { let wrapper = mount(Component) expect(wrapper.vm.status).toBe('loading') })
Cool! If that spec now runs, it should pass just fine, and the first test is written. But let’s look a little closer at what actually goes on here.
import { mount } from 'vue-test-utils'
— For us, this is the entry point to the vue-test-utils.mount
allows us to wrap that component and get access to the Vue instance and several helper methods around it.vue-test-utils
comes with more methods, but that will be the only one we need for now.let wrapper = mount(Component)
— That’s where we actually wrap our component. The mount method has more tricks up its sleeve, as you’ll see soon.expect(wrapper.vm.status).toBe('loading')
— The important piece of information here is definitelyvm
. That key gives you access to the actual mounted Vue instance. By default, Vue binds all props, computed properties, and data values as getters to the root object. So if you want to access any of those, that’s the way to go.
Okay, but now we want to make better use of the mount
functionality. Vue provides a propsData
key for passing in values for unit testing. Luckily those are supported as well in the vue-test-utils
.
test('the default status can be overwritten', () => { let wrapper = mount(Component, { propsData: { status: 'custom status' } }) expect(wrapper.vm.status).toBe('custom status') expect(wrapper.vm.localStatus).toBe('custom status') })
Snapshot testing
The next interesting element could be rendered output of our component. As mentioned earlier, there is a thing in Jest called snapshot testing.
test('rendered output should match snapshot', () => { let wrapper = mount(Component) expect(wrapper.html()).toMatchSnapshot() })
Running the spec the first time, Jest will print out to the console:
Snapshot Summary › 1 snapshot written in 1 test suite.
Before there was nothing to compare against, but now there is a snapshot. By default, those will be stored inside a __snapshot__
folder. That new snapshot should now be verified once to look as expected:
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendered output should match snapshot`] = ` "<div class=\\"loading\\"> Loading status </div>" `;
If all looks fine, that spec is now providing a lot of value for us; we can be sure that the rendered output won’t diverge from what our snapshot looks like. Be sure to check those snapshots into your code repository, so that every time the spec is run, we know it’s running against that snapshot.
Traversing the DOM
With vue-test-utils
, it’s also quite easy to traverse the DOM. That will be quite handy for testing events.
test('finding DOM nodes', () => { let wrapper = mount(Component) expect(wrapper.find('div').exists()).toBeTruthy() expect(wrapper.is('div')).toBeTruthy() expect(wrapper.contains('div')).toBeFalsy() expect(wrapper.findAll('div').length).toBe(1) })
As you see above, there are multiple ways to get information about your DOM through the wrapper. Know that .find
and .findAll
include the root node while .contains
is not.
With the newly found knowledge, we can try to find the root node and fire a click event on it. Thinking ahead though, we face a problem: We need to verify that the click worked. There is no value on the instance we could check afterward.
Mocking and spying
Also mentioned above, Jest comes with nice mocking functionality in place. Our component fires an alert
, so let’s spy on that one.
test('fire a click on a node', () => { global.alert = jest.fn() let wrapper = mount(Component) wrapper.find('div').trigger('click') expect(alert).toHaveBeenCalled() expect(alert).toHaveBeenCalledWith('Your current status: loading') })
That was a breeze, right? But in some cases, it could be quite hard to spy on whatever comes in that method, and ideally, we want to mock that function in the first place because there is an extra spec that handles only that function. That's a little trickier, but not much. Here it goes:
test.only('mock a function', () => { let mockedNotify = jest.fn() Component.methods.notify = mockedNotify let wrapper = mount(Component) wrapper.find('div').trigger('click') expect(mockedNotify).toHaveBeenCalled() })
The tricky part here is that we need to set up our mocks on the raw component object, as we can’t manipulate the Vue instance methods when created. To not lose track of the mocked function, it gets stored in an extra variable upfront. If it's expected that a function returns a value and it's more complex to mock everything inside, why not mock a return value? That's quite easy to do as well with Jest.
mockedNotify.mockImplementation(() => ‚Notified‘)
That would return the String Notified
whenever we call that method. Sweet!
Instance updates and timeout functions
There is one last thing we need to tackle before wrapping this up. In our component, a mounted function updates our localStatus
asynchronously. Of course, the computed property of statusMessage
will reevaluate and eventually the rendered output. So let’s see how we can tackle all those things at once.
Luckily, it's quite easy to mock timeout functions in Jest; it requires nothing more than adding jest.useFakeTimers()
at the top of the spec. Afterward, Jest gives you access to running timers, and they can be finished individually. For the ease of usage, we finish all running timers.
jest.useFakeTimers() test('status should be updated with good', () => { let wrapper = mount(Component) jest.runAllTimers() expect(wrapper.vm.localStatus).toBe('good') wrapper.update() expect(wrapper.html()).toMatchSnapshot() })
Here we’ve seen another important method of the wrapper instance: the update()
method. As Vue’s internal HTML updates depend on an internal Tick to minimize serenaders, this event runs asynchronously. The wrapper instance allows for calling an explicit update though. We match it against a new snapshot and enjoy our greatly tested component.
The snapshot should look something like this:
exports[`status should be updated with good 1`] = ` "<div class=\\"good\\"> Current status: GOOD
Ready for More?
There is a lot more ground to cover with the vue-test-utils
, such as shallow rendering or mocking nested components. Additionally, Jest allows for some advanced configurations and setup scripts. If you're interested in more discussion of Vue and Jest, I had the pleasure of speaking about testing Vue components with Jest at the first official Vue Conference in June 2017. The video of my talk is available here:
[youtube https://www.youtube.com/watch?v=pqp0PsPBO\_0&w=560&h=315\]
We're incredibly proud to be part of the Vue community by sponsoring the Vue Conference and want to say thank-you to all the other sponsors, like GitLab and NativeScript and especially our friends from Monterail. Without them, the event wouldn't have happened.
Please share your feedback in the comments below, and let me know what you'd prefer for future posts.