With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.
In this article, we’ll look at how to test Vue.js apps by writing a simple app and testing it.
Getting Started
To get started, we create an app that gets a joke from the Chuck Norris Jokes API.
We start by creating an empty folder, going into it, and running the Vue CLI by running:
npx vue create .
In the wizard, we select Unit Tests, then choose Jest and then proceed.
Now that we have the files generated, we can change some code. We can delete the components
folder and replace the code in App.vue
with:
<template>
<div id="app">
<button @click='toggleJoke()'>{{jokeHidden ? 'Show' : 'Hide'}} Joke</button>
<p v-if="!jokeHidden">{{data.value.joke}}</p>
</div>
</template>
<script>
export default {
name: "app",
data() {
return {
jokeHidden: false,
data: { value: {} }
};
},
beforeMount() {
this.getJoke();
},
methods: {
async getJoke() {
const res = await fetch("http://api.icndb.com/jokes/random");
this.data = await res.json();
},
toggleJoke() {
this.jokeHidden = !this.jokeHidden;
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
The code just gets a joke from the API and then display it. Also, it has a button to show and hide the joke.
Our app looks something like the following:
Creating the Tests
Now that we have something to test, we can actually write the tests.
In the tests/unit
folder, we delete what we have then create app.spec.js
in that folder.
Then we open the file we created and add:
import { mount } from '@vue/test-utils';
import App from '@/App.vue'
const mockResponse = {
"type": "success",
"value": {
"id": 178,
"joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",
"categories": []
}
}
To import the component that we’ll test, the mount
function to let the Vue Test Utils build and render the component for testing, and the mockResponse
object that we’ll use to set the mock data.
Then we add the skeleton for our test by writing:
describe('App.vue', () => {
beforeEach(() => {
jest.clearAllMocks()
})
})
We have the string description for our test suite and a callback which we add out tests to.
Inside the callback, we have the beforeEach
hook to clear all the mocks by running jest.clearAllMocks()
.
We need this because we’ll mock some of the functions in our component later.
Adding our First Test
Next, we write our first test. This test will simulate getting the data from the API and then displaying the joke on the screen.
It won’t actually get the joke from the server since we want our test to run anywhere and at any time. Getting it from the server won’t let us do that.
The API returns something different every time we call it and also it might not always be available.
With that in mind, we write:
it('renders joke', async () => {
const wrapper = mount(App, {
methods: {
getJoke: jest.fn()
}
});
wrapper.vm.data = mockResponse;
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)
})
in the callback we passed into the describe
function after the beforeEach
call.
The test above calls mount
on our App
component to build and render the component and returns a Wrapper
object to let us access it.
In the second argument, we pass in the options with the methods
property so that we can mock the getJoke
method with Jest with jest.fn()
. We want to mock it so that our test doesn’t call the API.
Once we have the wrapper
then we run:
wrapper.vm.data = mockResponse;
to set the mockResponse
data to the data
property of our component instance.
Once we did that, we check that we get the joke in our mockResponse
rendered by writing:
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)
since we put our joke in the p
tag in our App
component.
The expect
method and toMatch
are from Jest.
Writing Test that Interacts with UI Elements
Writing a test that does something to UI elements like buttons isn’t that much more work.
To test the button that we added to our app actually shows and hides the joke, we write:
it('toggles joke', () => {
const wrapper = mount(App, {
methods: {
getJoke: jest.fn()
}
});
wrapper.vm.data = mockResponse;
expect(wrapper.find('button').text()).toMatch('Hide Joke');
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');
expect(wrapper.find('button').text()).toMatch('Show Joke');
expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');
expect(wrapper.find('button').text()).toMatch('Hide Joke');
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);
}
)
The first part:
const wrapper = mount(App, {
methods: {
getJoke: jest.fn()
}
});
wrapper.vm.data = mockResponse;
is the same as before. We mock the getJoke
function with jest.fn()
so that our test won’t call the API. Then set the mock data.
Next, we check the button text by writing:
expect(wrapper.find('button').text()).toMatch('Hide Joke');
and that our mocked joke is shown in the p
element:
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);
Then we click our button by running:
wrapper.find('button').trigger('click');
And then check for the text of the button and whether the p
element is removed by our v-if
directive:
expect(wrapper.find('button').text()).toMatch('Show Joke');
expect(wrapper.find('p').exists()).toBe(false);
Finally, we can do the click again and check if the joke is shown again as follows:
wrapper.find('button').trigger('click');
expect(wrapper.find('button').text()).toMatch('Hide Joke');
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);
Running the Tests
Together, we have the following test code in app.test.js
:
import { mount } from '@vue/test-utils';
import App from '@/App.vue'
const mockResponse = {
"type": "success",
"value": {
"id": 178,
"joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",
"categories": []
}
}
describe('App.vue', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('renders joke', async () => {
const wrapper = mount(App, {
methods: {
getJoke: jest.fn()
}
});
wrapper.vm.data = mockResponse;
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)
})
it('toggles joke', () => {
const wrapper = mount(App, {
methods: {
getJoke: jest.fn()
}
});
wrapper.vm.data = mockResponse;
expect(wrapper.find('button').text()).toMatch('Hide Joke');
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');
expect(wrapper.find('button').text()).toMatch('Show Joke');
expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');
expect(wrapper.find('button').text()).toMatch('Hide Joke');
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);
})
})
Then we run the tests by npm run test:unit
.
We should get:
PASS tests/unit/app.spec.js
App.vue
√ renders joke (19ms)
√ toggles joke (11ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.102s
Ran all test suites.
every time that we run our tests since we mocked the data.
Conclusion
Vue CLI creates a project that has unit testing built-in if we choose to include it. This saves us lots of work.
Jest is an easy test runner with lots of features like mocking and expect
matchers that we can use.
To test UI components, we use the wrapper object returned by mount
, which has the rendered component. Then we can use find
to search the DOM for what we want to look for.
If the element exists, we can also trigger events on it by calling the trigger
method with the event that we want to fire.
Finally, we have the exists
method to check if the element we look for actually exists.