- Starting Code
-
Ending Code
lesson 3 - Starting Code
-
Ending Code
lesson4 - Starting Code
-
Ending Code
lesson5 - Starting Code
-
Ending Code
lesson6 - Starting Code
- Ending Code
What to test
Have you ever deployed a new feature in your app and crossed your fingers, hoping you don’t wake up to the news that you’ve accidentally introduced a brand new bug? You can greatly reduce these kinds of concerns, and strengthen your Vue apps, by testing your application.
A thoroughly tested application generally consists of a well implemented combination of several kinds of testing, including End to End (E2E) testing, sometimes Integration testing, Snapshot testing, and Unit Testing. This course specifically serves as a beginner’s guide to unit testing in Vue. As you’ll see throughout the course, unit tests serve as the foundation of a well tested app.
In this course, we’ll be using the popular Jest JavaScript testing library to run our tests, along with Vue Test Utils, the official unit testing utility library for Vue.
Goals of writing tests
In the following lesson, we’ll write our first test. But first, let’s gain a clear understanding of the benefits of testing your apps. What do we hope to gain from all of this added work of writing more code to test our code?
Boosted Confidence
Beyond sleeping better at night after a new feature deployment, testing helps bring everyone in your team onto the same page. If you’re new to a repository, seeing a suite of tests is like having an experienced developer sitting right next to you and watching you code, making sure you are staying within the proper lanes of what the code ought to do. With these tests in place, you can feel confident you aren’t breaking anything when adding new functionality or changing existing code.
Quality Code
When you write your components with testing in mind, you end up creating isolated, more reusable components. If you start writing tests for your components and you notice they aren’t easy to test, this is a clear sign that you can probably refactor your components, which can end up improving them.
Better Documentation
As alluded to in the first point, another result of testing is that it can end up producing good documentation for your development team. When someone is new to a code base, they can look to the tests for guidance, which can provide insight around the intentions for how the components should work, and provide clues to edge cases that may be tested for.
Identifying what to test
So we know that testing is valuable, but what exactly should we be testing in our applications? It’s easy to go overboard and test things you don’t have to, which unnecessarily slows down development time. So what do we test in a Vue.js app? The answer is actually quite simple: components. Since a Vue app is merely a puzzle of interlocking components, we need to be testing their individual behavior to make sure they’re working correctly.
The Component Contract
When first learning about Vue unit testing myself, I found it helpful when Ed Yerburgh (who literally wrote the book on Testing Vue.js Applications) spoke about thinking through the component contract . By that, we’re referring to the agreement between a component and the rest of the application.
For example, imagine a component that takes in a min
and max
prop and generates a random number between those prop values, which it renders to the DOM. The component contract says: I will accept two props and use them to produce a random number. Inherent within this contract is the concept of inputs and outputs . The component agrees to receive the min
and max
props as inputs and to deliver a random number as the output . As such, we can start to pick apart what we should be testing by thinking through the component contract, and identifying the inputs and outputs.
At a high level, common inputs and outputs are as follows:
Inputs
- Component Data
- Component Props
- User Interaction
- Ex: user clicks a button
- Lifecycle Methods
-
mounted()
,created()
, etc.
-
- Vuex Store
- Route Params
Outputs
- What is rendered to the DOM
- External function calls
- Events emitted by the component
- Route Changes
- Updates to the Vuex Store
- Connection with children
- i.e. changes in child components
By focusing on these specifically, you avoid focusing on the internal business logic. In other words, we shouldn’t be getting bogged down by worrying about how every single line of code works. It may seem counterintuitive, but the goal of unit testing is purely to ensure your components are producing the expected results. We aren’t concerned here about how it arrived at that result. We may even change the way we’re logically arriving at that result later on, so we don’t want our tests to be unnecessarily prescriptive about how that ought to be achieved. It’s up to your team to figure out the most efficient path to that result. That’s not the job of testing. As far as a unit test is concerned, if it works, it works.
Now that we know what we should be testing for, let’s look at a couple basic examples and identify what we might test in each.
Example: AppHeader Component
In this example, we have a component that will display a logout button if the loggedIn
property is true
.
AppHeader.vue
<template>
<div>
<button v-show="loggedIn">Logout</button>
</div>
</template>
<script>
export default {
data() {
return {
loggedIn: false
}
}
}
</script>
To get clear on what part of this component we ought to test, our first step is to identify the component’s inputs and outputs.
Inputs
- Data (
loggedIn
)- This data property determines if the button shows or not, so this is an input that we should be testing
Outputs
- Rendered Output (
button
)- Based on the inputs (
loggedIn
), is our button being displayed in the DOM when it should be?
- Based on the inputs (
With more complex components, there will be more aspects of it to test, but the same general approach applies. While getting a feel for what you ought to be testing is certainly helpful, so is knowing what you should not be testing. So let’s unpack that now.
What NOT to test
Understanding what doesn’t need to be tested is an important part of the testing story that many developers don’t think about, and in turn it costs them a lot of time that could be spent elsewhere.
Let’s look again at the example from earlier, where we have a component that takes in a min
and max
prop and outputs a random number within that range. We already know we should be testing the the output that is rendered to the DOM, and to do so we’ll need to be considering the min
and max
props in our test. But what about the actual method that is generating the random number? Do we need to test that?
The answer is no. Why? Because we don’t need to get lost in the implementation details.
Don’t test implementation details
When unit-testing we don’t need to fuss over how certain things work, just that they do work. We don’t need to set up a test that calls the function that generates our random number, making sure it behaves in a certain way. We don’t care about the internals here. We just care that the component produced the output we are expecting. This way, we can always come back later and replace the implementation logic (with a third-party library for generating random numbers, for example).
Don’t test the framework Itself
Developers often try to test too much, including the inner workings of the framework itself. But the framework authors already have tests set up to do that. For example, if we set up some prop validation for our min
and max
props, specifying they need to be a Number
, we can trust that Vue will throw an error if we try passing in a string. We don’t need to waste our time doing Evan You’s job and testing the Vue.js framework. This includes not doing needless tests on Vue Router and Vuex, too.
Don’t test third party libraries
If the third-party libraries you are using are of high quality, they will already have their own tests. We don’t need to test their internals. For example, we don’t need to test that Axios works how it should. The Axios team does that for us. These are the types of things that will bog us down if we worry about them unnecessarily. If you don’t feel you can trust the library you’re using to have been well tested, perhaps that’s a sign you might want to avoid using it to begin with.
Let’s ReVue
In this lesson, we took an important first step before writing effective unit tests: identifying what you should and shouldn’t be testing in components. With this approach, we can focus our time wisely on testing the pieces that need to be tested. In the next lesson, we’ll take this knowledge and write our first unit test.
Writing a Unit Test with Jest
In this lesson, we’re going to write our first unit test using Jest and Vue Test Utils. You can get started with the starting code available in this page’s lesson resources, or you can follow along and create the project from scratch using the Vue CLI.
Creating Our Project
We’ll create a new project using the Vue CLI. From the command line run the following command:
vue create unit-testing-vue
We’ll choose “Manually select features” and click enter so that we can specify which libraries we want to include in our new project. Since we will be learning how to test with Vue Router and Vuex , we’ll select both of those and of course we need to select Unit Testing. This will add the appropriate libraries to our project.
In the previous step, Linter / Formatter was selected by default, the next step allows us to customize that feature. For those, I selected ESLint + Prettier , and Lint on save . This configuration is totally up to you for this project.
Because we selected Unit Testing as a feature to include in our project, the next step asks what library we want to use for Unit Testing. We are going to use Jest .
We are going to put all of our configuration in their own dedicated files so you can leave the default here and press enter .
If you want to save this as a preset you can, if not type n and press enter . Our project will then be built for us.
Touring the Project Structure
With the project open, let’s start out by looking into the package.json , where we’ll see that Jest and vue-test-utils were installed for us.
package.json
"devDependencies": {
"@vue/cli-plugin-unit-jest": "^3.11.0",
"@vue/test-utils": "1.0.0-beta.29"
}
What do these libraries do again? As a reminder:
Jest is a JavaScript testing framework that focuses on simplifying your unit tests. Jest will run the unit tests for us and report back to us if the test passed or failed. While Jest is a pretty large framework (there are entire books written on the subject), you will only need to understand a few pieces to write some basic tests.
Vue Test Utils is the official unit testing utility library for Vue.js. It gives us the ability to render our components in our tests and then perform various operations on those rendered components. This is crucial for determining the actual results from a component’s behavior.
Great. We have the proper testing tools installed. But how do we get them to work? Notice this script command within our package.json :
package.json
"scripts": {
...
"test:unit": "vue-cli-service test:unit"
},
This command essentially looks into the directory called tests / unit and runs the tests we’ve set up in our ComponentName.spec.js files.
If we look inside our tests / unit directory, we’ll find an Example.spec.js file that was created for us. This is a dummy test file that is testing the HelloWorld.vue component in our src/components directory. For now, we’ll ignore what’s written inside that Example.spec.js file and go straight to our terminal and enter npm run test:unit
When we do that, we’ll see that the test within Example.spec.js is being run and it passes.
This is what we’re going to learn how to do by the end of this lesson. We’ll create a new component, set up some tests for it, and run those tests using the test:unit
script command.
A new component and testing file
Before writing any tests, we need a component to test. So we’ll delete out the HelloWorld.vue component in src/components and then create a new file called AppHeader.vue , which looks like this:
AppHeader.vue
<template>
<div>
<button v-show="loggedIn">Logout</button>
</div>
</template>
<script>
export default {
data() {
return {
loggedIn: false
}
}
}
</script>
This component should look pretty familiar because we used it as an example in the previous lesson. It is a simple App Header that displays a Logout button when a user is loggedIn
.
Now that we have a component to test, we can head into our test / unit directory, delete out the example testing file and create a new one called AppHeader.spec.js . As you can see in the naming convention here, we are using the name of the component we’re testing AppHeader + spec.js . Spec stands for specification because in this file we are essentially specifying how the AppHeader component ought to behave, and testing that behavior.
Note that these file names must include “spec.js” — without it, they won’t be run when we use the npm run test:unit
script.
Identifying what to test
Before we can write tests for our AppHeader.vue component, we need to identify its inputs and outputs . Fortunately, we already covered that in the previous lesson.
Inputs
- Data:
loggedIn
- This data property determines if the button shows or not
Outputs
- Rendered Output:
<button>
- Based on theloggedIn
input, is our button displayed in the DOM or not
We know that when loggedIn
equals false (default), the Logout button is not displayed in the DOM. When loggedIn
equals true, then the Logout button is displayed.
So our tests for this component are:
- If user is not logged in, do not show logout button
- If user is logged in, show logout button
Scaffolding our first unit test
Now that we know what we’re testing, we can head into our AppHeader.spec.js file and start writing our tests. First, we’ll need to import the component that we’re testing.
AppHeader.spec.js
import AppHeader from '@/components/AppHeader'
Now we can create our first test block by using the Jest describe()
function.
AppHeader.spec.js
describe('AppHeader', () => {
})
A describe block allows us to group related tests. When we run our tests we will see the name of the describe block printed in the console. As its arguments, describe()
takes a string for the name of the component along with a function where the tests will go. It’s worth noting here that if we only have one test we don’t need to wrap it in a describe block. But when we have multiple tests, it’s helpful to organize them in this way.
Now that we have a grouping for our tests, we can start writing those individual tests. We do this by using the Jest test()
method. For its arguments, the test method takes a string to define the test and a function where the actual testing logic will go.
AppHeader.spec.js
test('a simple string that defines your test', () => {
// testing logic
}
TIP: You might also see test blocks that use it()
and this will also work because it’s an alias for test()
.
So our two tests will start out looking like this:
AppHeader.spec.js
test('if user is not logged in, do not show logout button', () => {
// test body
})
test('if a user is logged in, show logout button', () => {
// test body
})
So we have the tests set up, but they don’t do anything yet. We need to add some logic in the body of them to determine if the actual result matches the result we are expecting.
Asserting Expectations
In Jest, we use assertions to determine whether what we expect the test to return matches what is actually returned. Specifically, we do this by using Jest’s expect()
method, which gives us access to a number of “matchers” that help us match the actual result against the expected result.
The syntax for an assertion basically works like this:
expect(theResult).toBe(true)
Inside the expect()
method, we’re putting the result itself that we’re testing. We then use a matcher to determine if that result is what we expected it to be. So here, we’re using the common Jest matcher toBe()
to say: we expect the result to be true.
When writing tests, it’s helpful to first write a test that you know will definitely pass (or definitely fail). For example, if we say: expect(true).toBe(true)
we know this will definitely pass. The result that’s passed into expect()
is true
and we are saying we expect that result toBe
true
. So if we were to run these tests, we know they will definitely pass because true
== true
.
AppHeader.spec.js
describe('AppHeader', () => {
test('if a user is not logged in, do not show the logout button', () => {
expect(true).toBe(true)
})
test('if a user is logged in, show the logout button', () => {
expect(true).toBe(true)
})
})
If these tests don’t pass, then we know we’ve set something else up wrong elsewhere in our code. So writing this kind of test serves as a form of sanity test for us, preventing us from wasting time debugging our testing code when tests that should be passing are not.
Understanding how to write tests means understanding what matchers are available to you, so take some time to understand the Jest Matchers API.
The Power of Vue Test Utils
Now that we’ve scaffolded our tests, and got them both passing, we can switch out that “sanity test” with the real logic that can perform our given tests:
- If a user is not logged in, do not show the logout button
- If a user is logged in, show the logout button
In order to do this, we need the AppHeader component to be mounted (to check if the button is visible in the DOM or not). This would be quite a process to perform all on our own, but fortunately with the help of Vue Test Utils, it’s very simple because this library comes packaged with mount
.
So let’s import mount
into our testing file and see what it can do for us.
AppHeader.spec.js
import { mount } from '@vue/test-utils'
import AppHeader from '@/components/AppHeader'
describe('AppHeader', () => {
test('if user is not logged in, do not show logout button', () => {
const wrapper = mount(AppHeader) // mounting the component
expect(true).toBe(true)
})
test('if user is logged in, show logout button', () => {
const wrapper = mount(AppHeader) // mounting the component
expect(true).toBe(true)
})
})
Above, in each of our tests we’ve created a wrapper
const in which we mount(AppHeader)
. The reason we’re calling it wrapper
is because in addition to mounting the component, this method creates a wrapper that includes methods to test the component. Of course, it’s helpful to understand the different properties and methods on the wrapper, so take some time to explore the documentation.
Sidenote: In the Vue Test Utils you will also find the method shallowMount()
. If your component has children, shallowMount()
will return a simple implementation of that component instead of a fully rendered version. This is important because the focus of a unit test is the component in isolation and not the children of that component.
We’re still not performing our actual tests yet, but now we have a wrapper
of the mounted AppHeader component, which we can use to write out the complete tests.
Testing the Button’s visibility
In our first test case, we know that by default the user is not logged in (our input is loggedIn: false
) so we want to check and make sure the logout button is not visible.
To make assertions on the state of the logout button, we’ll need to get a reference to the button element that is defined in the template. To accomplish this we will rely on two methods available to us on our new wrapper : find()
and isVisible()
. The find()
method will search through our template for a matching selector in order to locate our button, and isVisible()
will return a boolean, telling us if that button is visible in our component or not.
So our first test will look like:
AppHeader.spec.js
test('if user is not logged in, do not show logout button', () => {
const wrapper = mount(AppHeader)
expect(wrapper.find('button').isVisible()).toBe(false)
})
For our second test, we want to find the button in the same way, but this time we expect it to be visible, so we’ll say: toBe(true)
.
AppHeader.spec.js
test("if logged in, show logout button", () => {
const wrapper = mount(AppHeader)
expect(wrapper.find('button').isVisible()).toBe(true)
})
Because we are testing the components behavior when we have a user (when loggedIn
is true
), we need to update that value or else this test will fail. How we do we this? Vue Test Utils to the rescue!
AppHeader.spec.js
test("if logged in, show logout button", () => {
const wrapper = mount(AppHeader)
wrapper.setData({ loggedIn: true }) // setting our data value
expect(wrapper.find('button').isVisible()).toBe(true)
})
Here, we’re using the wrapper ’s built-in setData()
method to set our data to fit the correct scenario that we’re testing. Now, when we run our tests in the terminal with npm run test:unit
, they should both be passing!
Let’s ReVue
We just covered a lot of steps, as a recap here is what we just did:
Obviously, every component that we are testing is a bit different so these steps may change, especially step 4. Instead of setting the data, we may need to set the props, or simulate user interaction, for example. And we’ll cover more of these test cases in future lessons.
Testing Props & User Interaction
In the previous lesson, we learned how to write and run our first unit tests. In this lesson, we’ll continue writing simple tests for a component that requires user interaction and takes in some props.
Random Number Component
In lesson 1, we identified the inputs and outputs of a component that generates a random number within the range of its min
and max
props.
Here is the code for that component:
src/components/RandomNumber.vue
<template>
<div>
<span>{{ randomNumber }}</span>
<button @click="getRandomNumber">Generate Random Number</button>
</div>
</template>
<script>
export default {
props: {
min: {
type: Number,
default: 1
},
max: {
type: Number,
default: 10
}
},
data() {
return {
randomNumber: 0
}
},
methods: {
getRandomNumber() {
this.randomNumber = Math.floor(Math.random() * (this.max - this.min + 1) ) + this.min;
}
}
}
</script>
What tests should we write?
In terms of this component’s inputs, it’s pretty obvious that the props are inputs since props are literally fed into the component. Another input would be the user interaction, whether a user clicked the button or not, which runs the method that generates the random number. The output is the rendered HTML displaying the randomNumber
.
Inputs
Props:
-
min
&max
User Interaction:
- Clicking of the Generate Random Number button
Outputs
Rendered Output (DOM)
- Is the number displayed on the screen between min and max?
We can use this knowledge to figure out what to test in this component:
- By default, the randomNumber data value should be
0
- If we click the generate button,
randomNumber
should be between1
(min) and10
(max) - If we change the min and max props to
200
and300
and click the button,randomNumber
should be between200
(min) and300
(max).
Random Number Tests
To get started testing this component, we’ll created a new test file: /tests/unit/RandomNumber.spec.js
For now, we’ll simply scaffold out the tests and write default assertions that we know will fail. In the last lesson, we looked at scaffolding our tests with an assertion we know would pass. By using an assertion we know will definitely fail, this serves a similar purpose of making sure our components are behaving how we expect them to at first. Then, we’ll do the work to make the tests pass.
**/tests/unit/RandomNumber.spec.js**
import { mount } from '@vue/test-util'
import RandomNumber from '@/components/RandomNumber'
describe('RandomNumber', () => {
test('By default, randomNumber data value should be 0', () => {
expect(true).toBe(false);
})
test('If button is clicked, randomNumber should be between 1 and 10', () => {
expect(true).toBe(false);
})
test('If button is clicked, randomNumber should be between 200 and 300', () => {
expect(true).toBe(false);
})
})
If we run npm run test:unit
, we’ll see 3 failures for this test.
Checking the default random number
Looking at the component, we know that the default value for randomNumber
is 0
, so why would you even test for this? Well, what if someone else on our team changed the default randomNumber
? Testing it gives us confidence 0 will always be displayed when the component is first loaded.
Our first step to test this is to mount
the component we are testing ( RandomNumber.vue ), which gives us the wrapper that lets us dive into the component and test what we need to. The test is going to look like this:
/tests/unit/RandomNumber.spec.js
test('By default, randomNumber data value should be 0', () => {
const wrapper = mount(RandomNumber)
expect(wrapper.html()).toContain('<span>0</span>')
})
Here, we’re using the wrapper to get the html
of our RandomNumber component, and asserting that we expect
that html toContain
a span with a 0 in its inner HTML.
If we run this test by typing npm run test:unit in our terminal, we’ll see that now only 2 tests are failing, and we’ve gotten our first test to pass. Now we can move on to our next test, which requires some user interaction.
Simulating User Interaction
We need to verify that when we click the generate random number button, we get a random number between the min
and max
props. The defaults for min
and max
are 1
and 10
respectively, so the random number should fall in that range.
Just as we have before, we’ll need to mount
the RandomNumber component. The new concept here is that we need to trigger a button click on our generate random number button (which runs the method that uses the min
and max
props to generate a new randomNumber
).
We’ll get a reference to the button element using the find()
method, and then use the trigger()
method to trigger an event on the wrapper DOM node. The first and required argument to the trigger
method is a string for what event type to trigger. In this case, we want to trigger a 'click'
event on the button.
/tests/unit/RandomNumber.spec.js
test('If button is clicked, randomNumber should be between 1 and 10', () => {
const wrapper = mount(RandomNumber)
wrapper.find('button').trigger('click')
})
Now that the button was ‘clicked’, it should have generated a random number. In order to access that number within the rendered html, we can write:
const randomNumber = parseInt(wrapper.find('span').element.textContent)
This effectively finds the span, and accesses that element’s text content. But because we need that content to be an integer, we’ve wrapped in in parseInt
.
Finally, we can use some Jest assertions to make sure the random number falls between the min prop 1
and max prop 10
.
/tests/unit/RandomNumber.spec.js
test('If button is clicked, randomNumber should be between 1 and 10', () => {
const wrapper = mount(RandomNumber)
wrapper.find('button').trigger('click')
const randomNumber = parseInt(wrapper.find('span').element.textContent)
expect(randomNumber).toBeGreaterThanOrEqual(1)
expect(randomNumber).toBeLessThanOrEqual(10)
})
If we run the tests now, we should see 2 passing. Now we can work on the final test.
Setting different prop values
Since someone using this component can change the min
and max
value range via the min
and max
props, we need to test for this. To do this, we’ll use the mount()
method, which takes an optional second argument, where we can pass in some options, including propsData
. This can be used to reset the min
and max
values, to 200
and 300
respectively, in our case.
/tests/unit/RandomNumber.spec.js
test('If button is clicked, randomNumber should be between 1 and 10', () => {
const wrapper = mount(RandomNumber, {
propsData: {
min: 200,
max: 300
}
})
})
With our new min
and max
, the test is going to look pretty similar to the one we just wrote above.
/tests/unit/RandomNumber.spec.js
test('If button is clicked, randomNumber should be between 200 and 300', () => {
const wrapper = mount(RandomNumber, {
propsData: {
min: 200,
max: 300
}
})
wrapper.find('button').trigger('click')
const randomNumber = parseInt(wrapper.find('span').element.textContent)
expect(randomNumber).toBeGreaterThanOrEqual(200)
expect(randomNumber).toBeLessThanOrEqual(300)
})
At this point you can run your tests and all of them should pass. Congratulations!
Let’s Revue
Not all components are created equally, so testing them often means we’ll have to consider things like simulating button clicks, and testing props. In our next lesson,we’ll continue learning about testing common aspects of Vue components.
Testing Emitted Events
In the previous lesson, we looked at testing a component that took in some props and generated a number based upon a button click. This required us to simulate ( trigger
) the button click within our test. That “click” falls under the category of a native DOM event, but often in Vue components, we need our components to emit their own custom event, and in this lesson we’ll look at testing such events.
What’s a custom event?
If custom events are new to you, you can check out the Communicating Events lesson in our Intro to Vue.js course. In short: sometimes a child component needs to let another component in the app know that something happened within it. It can broadcast that something happened by emitting a custom event (such as formSubmitted
) to let its parent know that event happened. The parent can wait, listening for that event to happen, then respond accordingly when it does.
The Starting Code
Fortunately, Vue Test Utils provides us with an emitted API, which we can use to test these kinds of custom emitted events within our components. We’ll explore how to use that API in a moment, but first let’s look at the component that we’ll be testing.
LoginForm.vue
<template>
<form @submit.prevent="onSubmit">
<input type="text" v-model="name" />
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
name: ''
}
},
methods: {
onSubmit() {
this.$emit('formSubmitted', { name: this.name })
}
}
}
</script>
As you can see, we a very simple login form. Pay attention to how we have the onSubmit()
method, which is used to $emit
a custom event by the name of formSubmitted
, which sends a payload containing the name
data, which is bound to our input
element. We want to test that when the form iis submitted, it indeed emits an event, containing the user name
payload.
Scaffolding the test file
When writing tests, it’s helpful if we write them in a way that mimics how an actual end user will interact with the component. So how will a user use this component? Well, they will find the text input field, then they’ll add their name, then submit the form. So we’ll replicate that process as closely as possible in our test, like so:
LoginForm.spec.js
import LoginForm from '@/components/LoginForm.vue'
import { mount } from '@vue/test-utils'
describe('LoginForm', () => {
it('emits an event with a user data payload', () => {
const wrapper = mount(LoginForm)
// 1. Find text input
// 2. Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})
Above, you can see that we’ve imported the LoginFrom component along with mount
from Vue Test Utils, and we need to accomplish the following steps:
- Find text input
- Set value for text input
- Simulate form submission
- Assert event has been emitted
- Assert payload is correct
Let’s implement these steps.
Setting the text input value
First, just like our end user would, we’ll find the text input and set its value.
LoginForm.spec.js
describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input
input.setValue('Adam Jahr') // Set value for text input
// 3. Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})
A note about targeting inputs
This works great for our specific needs, but it’s worth mentioning here that in production tests, you might considering using test-specific attribute on your elements, like so:
<input data-testid="name-input" type="text" v-model="name" />
Then, in your test file, you’d find the input
using that attribute.
const input = wrapper.find('[data-testid="name-input"]')
This is beneficial for a few reasons. First, if we had multiple inputs we could target them specifically with these ids, and perhaps more importantly this decouples the DOM from your test. For example, if you eventually replaced the native input with an input from a component library, the test would still conform to the same interface and wouldn’t need to change. It also solves the issue of a designer changing the class or id name of the element, causing your test to fail. Test-specific attributes are one way to future-proof your tests.
Simulating the form submission
Once our end user has filled out our form, the next step they would take would be to submit the form. In the previous lesson, we looked at how the trigger
method can be used to simulate an event on a DOM element.
While you might be tempted to use trigger
on our form’s button to simulate our form submission, there’s a potential problem with that. What if we eventually removed the button from this component and instead relied on the input’s keyup.enter
event to submit the form? We would have to refactor our test. In other words: in that case our test would have been too tightly coupled with the implementation details of our component’s form. So the more future-proofed solution would be to force a submit even on the form itself, without relying on our button as the middle man.
LoginForm.spec.js
describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input
input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission
// 4. Assert event has been emitted
// 5. Assert payload is correct
})
})
Now by using wrapper.trigger('submit')
, we’ve implemented a more scalable, decoupled solution to simulate a user submitting our form.
Testing our expectations
Now that our input field’s value has been set and our form has been submitted, we can move on to testing that what we expect to happen is actually happening:
- The event has been emitted
- The payload is correct
To test the event was emitted, we’ll write:
LoginForm.spec.js
describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input
input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission
// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)
})
})
Here we are using Vue Test Utils’ emitted API to store any calls of the formSubmitted
event in a const, and asserting that we expect
that array to have a length of 1
. In other words: we are checking to see if the event was indeed emitted.
Now we just need to confirm that the event was emitted with the proper payload (our component’s name
data value). We’ll use the emitted API again for this.
If we were to console.log wrapper.emitted('formSubmitted')
, we’d see this:
[[], [{ 'name': 'Adam Jahr' }]]
So in order to target the payload itself, the syntax would look like:
wrapper.emitted('formSubmitted')[0][0])
We’ll then match that against our expected payload, which for organization purposes we’ll store in const expectedPayload = { name: 'Adam Jahr' }
Now we can check to see that our expectedPayload
object matches the payload emitted along with the formSubmitted
event.
const expectedPayload = { name: 'Adam Jahr' }
expect(wrapper.emitted('formSubmitted')[0][0]).toMatchObject(expectedPayload)
Sidenote: We could alternatively hard-code the expected payload into the matcher: .toEqual({ name: 'Adam Jahr' })
. But storing it in a const keeps us a bit more clear about what is what.
Our full testing file now looks like this:
LoginForm.spec.js
import LoginForm from '@/components/LoginForm.vue'
import { mount } from '@vue/test-utils'
describe('LoginForm', () => {
it('emits an event with user data payload', () => {
const wrapper = mount(LoginForm)
const input = wrapper.find('input[type="text"]') // Find text input
input.setValue('Adam Jahr') // Set value for text input
wrapper.trigger('submit') // Simulate form submission
// Assert event has been emitted
const formSubmittedCalls = wrapper.emitted('formSubmitted')
expect(formSubmittedCalls).toHaveLength(1)
// Assert payload is correct
const expectedPayload = { name: 'Adam Jahr' }
expect(wrapper.emitted('formSubmitted')[0][0]).toMatchObject(expectedPayload)
})
})
If we npm run test:unit
we’ll see that our new test is passing. Great work!
Let’s ReVue
We’ve learned how to write a test while replicating how a user would interact with a component, in order to test that our custom events are emitted with the correct payload.
Testing API Calls
Unless you’re working with a simple static website, your Vue app is likely making API calls from within certain components. In this lesson, we’ll look at how we can test these kinds of data-fetching components.
The first thing to understand about testing components that make API calls is that we don’t want to be making real calls out to our backend. Doing so would couple our unit tests to the backend. This becomes an issue when we want to execute our unit tests in Continuous Integration. Real backends can also be unreliable, and we need our tests to behave predictably.
We want our tests to be fast and reliable, and we can achieve that by mocking our API calls and simply focusing on the inputs and outputs of the component we’re testing. In this lesson we’ll be using axios, the popular promise-based HTTP client, to make our calls. This means we’ll have to mock axios’ behavior, which we’ll do in a moment. But first let’s take a look at the starting code.
The Starting Code
For the sake of simplicity, instead of plugging into a full backend, we’re using json-server, which gives us a fake REST API. If this library is new to you, we taught it over in our Real World Vue course. What you need to know for this lesson is: our db.json file is our database, and json-server can fetch data from it.
Our simple db has one endpoint: “message”, and this is the data we’ll be fetching.
db.json
{
"message": { "text": "Hello from the db!" }
}
In our project, I’ve also added an API service layer, which will handle the actual API calls.
services/axios.js
import axios from 'axios'
export function getMessage() {
return axios.get('http://localhost:3000/message').then(response => {
return response.data
})
}
As you can see, we’ve imported axios and we are exporting the getMessage()
function, which makes a get
request to our endpoint: http://localhost:3000/message
, then we’re returning the data from the response.
Now that we understand how and where we’re pulling our data from, we can look at the component that triggers this API call, and displays the returned data.
MessageDisplay.vue
<template>
<p v-if="error" data-testid="message-error">{{ error }}</p>
<p v-else data-testid="message">{{ message.text }}</p>
</template>
<script>
import { getMessage } from '@/services/axios.js'
export default {
data() {
return {
message: {},
error: null
}
},
async created() {
try {
this.message = await getMessage()
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
}
</script>
In the <script>
section, we’ve imported the getMessage
function from our axios.js file, and when our component is created
, it calls getMessage
using async
/ await
since axios is asynchronous and we need to wait for the promise it returns to resolve. When it resolves, we’re setting our component’s local message
data equal to the resolved value, which gets displayed in the template.
We’re also wrapping the getMessage
call with try
in order to catch
errors that might happen, and if one does occur, we’re setting our local error
data accordingly and displaying that error.
Inputs & Outputs
Looking at the MessageDisplay.vue component, what are the inputs and outputs that we’ll need to consider when writing our test?
Well, we know that the response from the getMessage
call is our input, and we have two possible outputs:
- The call happens successfully and the message is displayed
- The call fails and the error is displayed
So in our test file, we’ll need to:
- Mock a successful call to
getMessage
, checking that themessage
is displayed - Mock a failed call to
getMessage
, checking that theerror
is displayed
Let’s get started with learning how to mock axios.
Mocking Axios
Let’s scaffold the test block, import the component we’re testing, mount it, and use comments to piece apart what our tests need to be doing.
MessageDisplay.spec.js
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
describe('MessageDisplay', () => {
it('Calls getMessage and displays message', async () => {
// mock the API call
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays message
})
it('Displays an error when getMessage call fails', async () => {
// mock the failed API call
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays error
})
})
So let’s fill out these tests, one by one. Looking at the test where it “Calls getMessage and displays message”, our first step is to mock axios. Again, when testing components that make API calls, we don’t want to be making actual calls out to our database. We can get away with simply pretending we made the call by mocking that behavior, using Jest’s mock function.
In order to mock our API call, we’ll first import the getMessage
function from our axios.js file. We can then feed that function to jest.mock()
by passing it the path for where that function lives.
MessageDisplay.spec.js
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
jest.mock('@/services/axios')
...
You can think of jest.mock
as saying: “I’ll take your getMessage
function, and in return I’ll give you a mocked getMessage
function.” Now, when we call getMessage
within our tests, we’re actually calling the mocked version of that function, not the actual one.
So let’s call our newly mocked getMessage
function from within our first test.
MessageDisplay.spec.js
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
jest.mock('@/services/axios')
describe('MessageDisplay', () => {
it('Calls getMessage and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage }) // calling our mocked get request
const wrapper = mount(MessageDisplay)
// wait for promise to resolve
// check that call happened once
// check that component displays message
})
})
By using jest’s [mockResolvedValueOnce()](https://jestjs.io/docs/en/mock-function-api.html#mockfnmockresolvedvalueoncevalue)
method, we’re doing exactly what the method name suggests: pretending to make the API call and returning a mocked value for the call to resolve with. As its argument, this method takes in the value we want this mocked function to resolve with. In other words, this is where we put a stand-in for what the request should’ve returned. So we’ll pass in { text: mockMessage }
to replicate what the server would respond with.
As you can see, we’re using async
like we have in previous tests, because axios (and our mocked axios call) is asynchronous. This means that before we write any assertions, we’ll need to make sure that the promise that our mocked call returns gets resolved. Otherwise, our tests would run before the promise is resolved, and fail.
Awaiting Promises
When figuring out where to await
in our test, we have to think back to how getMessage
is being called in the component we’re testing. Remember, it’s being called on the component’s created
lifecycle hook?
MessageDisplay.vue
async created() {
try {
this.message = await getMessage()
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
Since vue-test-utils doesn’t have access to the internals of promises that are enqueued by the created
lifecycle hook, we can’t really tap into anything to await
for that promise. So the solution here is to use a third-party library called flush-promises which allows us to—well—flush the promises, ensuring they’re all resolved prior to running our assertions.
Once we’ve installed the library with npm i flush-promises --save-dev
, we’ll import it into our testing file and await
the flushing of the promises.
MessageDisplay.spec.js
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
import flushPromises from 'flush-promises'
jest.mock('@/services/axios')
describe('MessageDisplay', () => {
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage })
const wrapper = mount(MessageDisplay)
await flushPromises()
// check that call happened once
// check that component displays message
})
})
Now that we’ve ensured promises will be resolved before our assertions are run, we can write those assertions.
Our Assertions
First up, we’ll want to make sure our API call is only happening once.
MessageDisplay.spec.js
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce(mockMessage)
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1) // check that call happened once
// check that component displays message
})
We’re simply running the method .toHaveBeenCalledTimes()
and passing in the number of times we expect getMessage
to have been called: 1
. Now we’ve ensured that we aren’t accidentally hitting our server more times than we should be.
Next up, we need to check that our component is displaying the message it received from our getMessage
request. In the MessageDisplay component’s template, the p
tag that displays the message has an id to be used for tests: data-testid="message"
MessageDisplay.vue
<template>
<p v-if="error" data-testid="message-error">{{ error }}</p>
<p v-else data-testid="message">{{ message }}</p>
</template>
We learned about these testing ids in the previous lesson. We’ll use that id to find
the element, then assert that its text content should be equal to the value our mocked getMessage
request resolved with: mockMessage
MessageDisplay.spec.js
it('Calls getMessage once and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage })
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const message = wrapper.find('[data-testid="message"]').element.textContent
expect(message).toEqual(mockMessage)
})
If we run npm run test:unit
in the terminal, we’ll see our newly written test is passing! We can now move on to our second test, where we’ll mock a failed getMessage
request and check that our component is displaying the error.
Mocking a failed request
The first step, of mocking the failed API call, is very similar to our first test.
MessageDisplay.spec.js
it('Displays an error when getMessage call fails', async () => {
const mockError = 'Oops! Something went wrong.'
getMessage.mockRejectedValueOnce(mockError)
const wrapper = mount(MessageDisplay)
await flushPromises()
// check that call happened once
// check that component displays error
})
Notice how we’re using mockRejectedValueOnce
to simulate the failed get request, and we’re passing it the mockError
for it to resolve with.
After awaiting the flushing of the promises, we can then check that the call only happened once and verify that our component’s template is displaying the expected mockError
.
MessageDisplay.spec.js
it('Displays an error when getMessage call fails', async () => {
const mockError = 'Oops! Something went wrong.'
getMessage.mockRejectedValueOnce(mockError)
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const displayedError = wrapper.find('[data-testid="message-error"]').element
.textContent
expect(displayedError).toEqual(mockError)
})
Just like our first test, we’re using .toHaveBeenCalledTimes(1)
to make sure we’re not making the API call more than we should be, and we’re finding the element that displays the error message and checking its text content against the mockError
that our mocked failed request returned.
Now if we run these tests, what happens? The test is failing:
Expected number of calls: 1 Received number of calls: 2
Hmm… what’s happening here? Well, in our first test, getMessage
was called, and then it gets called again in our second test. We haven’t done anything to clear out our mocked getMessage
function before running the second test. Fortunately, the fix is quite simple.
Clear All Mocks
Below where we’re creating our jest mock, we can add the solution, clearing all of our mocks.
MessageDisplay.spec.js
jest.mock('@/services/axios')
beforeEach(() => {
jest.clearAllMocks()
})
Now, beforeEach
test is run, we’ll make sure the getMessage
mock has been cleared, which will reset the number of times it’s been called back to 0.
Now, when we run our tests, they’re all passing. Great work!
The Full Code
MessageDisplay.spec.js
import MessageDisplay from '@/components/MessageDisplay'
import { mount } from '@vue/test-utils'
import { getMessage } from '@/services/axios'
import flushPromises from 'flush-promises'
jest.mock('@/services/axios')
beforeEach(() => {
jest.clearAllMocks()
})
describe('MessageDisplay', () => {
it('Calls getMessage and displays message', async () => {
const mockMessage = 'Hello from the db'
getMessage.mockResolvedValueOnce({ text: mockMessage })
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const message = wrapper.find('[data-testid="message"]').element.textContent
expect(message).toEqual(mockMessage)
})
it('Displays an error when getMessage call fails', async () => {
const mockError = 'Oops! Something went wrong.'
getMessage.mockRejectedValueOnce(mockError)
const wrapper = mount(MessageDisplay)
await flushPromises()
expect(getMessage).toHaveBeenCalledTimes(1)
const displayedError = wrapper.find('[data-testid="message-error"]').element
.textContent
expect(displayedError).toEqual(mockError)
})
})
Let’s ReVue
We’ve learned that when testing API calls, the same essential rules apply: focus on the component’s inputs (the request’s response) and outputs (the message or error that’s displayed), while being conscious of avoiding tight coupling between the test and the implementation details of the component (finding the element by its testing id versus finding it by its element type, for example). We also learned how to use jest to mock our calls and the third party library flush-promises to await asynchronous behavior in our lifecycle hooks.
In the next lesson, we’ll learn what the heck a stub is and how it can help us test parent components.
Stubbing Child Components
In the previous lesson, we looked at mocking our axios module in order to test that our component was set up to make an API call and use the data that was returned, without ever having to hit our actual sever or unnecessarily coupling our test to our backend.
This concept of mocking something within our unit test is a broader topic than just mocking modules, whether they be axios or some other external dependency. In this lesson, we’ll delve deeper into this topic and look at another form of faking something within a component’s test, called stubbing, and why and when this approach might be useful.
Children with Baggage
To explore this concept, I’d like to introduce you to MessageContainer , the parent of MessageDisplay (the component we tested in the previous lesson).
MessageContainer.vue
<template>
<MessageDisplay />
</template>
<script>
import MessageDisplay from '@/components/MessageDisplay'
export default {
components: {
MessageDisplay
}
}
</script>
As you can see, MessageContainer simply imports and wraps MessageDisplay . This means that when MessageContainer gets rendered, MessageDisplay is also rendered. So we’re hitting up against the same problem from our previous lesson. We don’t want to actually fire the real axios get
request that happens on MessageDisplay ’s created
hook.
MessageDisplay.vue
async created() {
try {
this.message = await getMessage() // Don't want this to fire in parent test
} catch (err) {
this.error = 'Oops! Something went wrong.'
}
}
So what’s the solution here? How do we test MessageContainer without triggering its child’s axios request? Or to make the question more general: What do we do when a child component has module dependencies that we don’t want to use the real version of within our tests?
The answer to that is perhaps not a very satisfying one. Because the answer is: it depends. It depends on the complexity and number of modules that the child has. For this example, things are fairly lightweight. We only have one module, so we could simply mock axios in MessageContainer ’s test, just like we did in our MessageDisplay.spec.js . But what if our child component had multiple module dependencies? In more complex cases, it’s often the simpler approach to skip mocking the child component’s module baggage and instead mock the child component itself. In other words: we can use a stub , or fake placeholder version, of the child component.
With all of that intellectual boilerplate out of the way, let’s continue with this example and see how we would stub out MessageDisplay within MessageContainer ’s test.
The MessageContainer Test
We’ll start off with a scaffold:
MessageContainer.spec.js
import MessageContainer from '@/components/MessageContainer'
import { mount } from '@vue/test-utils'
describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer)
})
})
In this test, where and how do we stub our child? Remember it’s the mounting of MessageContainer that would have created
and mounted its child along with it, causing the child’s API call to fire. So it makes sense that we stub the child component when we mount
its parent. To do that, we’ll add a stubs
property as a second argument in our mount
method.
MessageContainer.spec.js
import MessageContainer from '@/components/MessageContainer'
import { mount } from '@vue/test-utils'
describe('MessageContainer', () => {
it('Wraps the MessageDisplay component', () => {
const wrapper = mount(MessageContainer, {
stubs: {
MessageDisplay: '<p data-testid="message">Hello from the db!</p>'
}
})
})
})
Notice how we’ve added our MessageDisplay
component to the stubs
property, and its value is the HTML we’d expect to be rendered if the child were to actually be mounted. Again, this stub is a placeholder that gets mounted when we mount the parent. It’s a canned response; a substitute standing in for the real child component.
Now, in order to ensure MessageContainer is doing its ****job of wrapping the MessageDisplay component, we need to look inside of what was mounted and see if the correct message from (our stubbed version of) MessageDisplay can be found.
MessageContainer.spec.js
describe('MessageContainer', () => {
it('Wraps MessageDisplay component', () => {
const wrapper = mount(MessageContainer, {
stubs: {
MessageDisplay: '<p data-testid="message">Hello from the db!</p>'
}
})
const message = wrapper.find('[data-testid="message"]').element.textContent
expect(message).toEqual('Hello from the db!')
})
})
We’ll create a const to store the stubMessage
we are expecting to be rendered, and in our assertion we’ll compare that against the message
that was mounted (from the stub).
If we run npm run test:unit
in our terminal, we indeed see that our test is passing and we’ve confirmed that MessageContainer is doing its job: wrapping the (stubbed) MessageDisplay component, which displays what the real child would have. Great.
The Disadvantages of Stubbing
While stubs can help us simplify tests containing otherwise burdensome child components, we need to take a moment to consider the disadvantages of stubbing.
Since a stub is really just a placeholder for the child component, if that real component’s behavior changed, we may need to then update the stub accordingly. This can lead to maintenance costs for upkeeping our stubs as our app evolves. Generally speaking, stubs can create a coupling between the test and component’s implementation details.
Furthermore, since a stub isn’t the actual fully rendered component, you are reducing test coverage of your real component codebase, which can lead to reduced confidence that your tests are giving you truthful feedback about your app.
I bring up these points not to discourage against using stubs, but to encourage you to use them wisely and sparingly, remembering that it’s often the better practice to focus on mocking modules and service layers like we saw in the previous lesson.
What about ShallowMount?
You may have seen shallowMount
being used in other people’s test code. And you may have heard that it’s a handy way for only mounting the top-level parent and not its children (thus: shallow and not deep down into child layers). So why aren’t we using that? Why are we manually stubbing out our children?
First, shallowMount
falls victim to the same disadvantages (if not more) that stubs have: reduced confidence and increased coupling and maintenance. Secondly, if you start to use other libraries that sit on top of Vue Test Utils, such as Vue Testing Library, you’ll find that shallowMount
isn’t supported. That’s why I’ve avoided teaching it. For more information on this, you can take a look at this article by Testing Library maintainer Kent C. Dodds.
Wrapping Up
As we conclude this lesson, we also conclude this course. I hope you’ve learned a lot in this introduction to unit testing your Vue apps. There are plenty more testing topics and production-level practices to cover, and you can look forward to learning more in our upcoming Unit Testing for Production course, which will be released in the coming months. Stay tuned!