This book is written for Vue.js 3 and Vue Test Utils v2.
Find the Vue.js 2 version here.
# Triggering Events
One of the most common things your Vue components will be doing is listening for inputs from the user. vue-test-utils
and Jest make it easy to test inputs. Let's take a look at how to use trigger
and Jest mocks to verify our components are working correctly.
The source code for the test described on this page can be found here.
# Creating the component
We will create a simple form component, <FormSubmitter>
, that contains an <input>
and a <button>
. When the button is clicked, something should happen. The first example will simply reveal a success message, then we will move on to a more interesting example that submits the form to an external endpoint.
Create a <FormSubmitter>
and enter the template:
<template>
<div>
<form @submit.prevent="handleSubmit">
<input v-model="username" data-username>
<input type="submit">
</form>
<div
class="message"
v-if="submitted"
>
Thank you for your submission, {{ username }}.
</div>
</div>
</template>
When the user submits the form, we will reveal a message thanking them for their submission. We want to submit the form asynchronously, so we are using @submit.prevent
to prevent the default action, which is to refresh the page when the form is submitted.
Now add the form submission logic:
<script>
export default {
name: "FormSubmitter",
data() {
return {
username: '',
submitted: false
}
},
methods: {
handleSubmit() {
this.submitted = true
}
}
}
</script>
Pretty simple, we just set submitted
to be true
when the form is submitted, which in turn reveals the <div>
containing the success message.
# Writing the test
Let's see a test. We are marking this test as async
- read on to find out why.
import { mount } from "@vue/test-utils"
import FormSubmitter from "@/components/FormSubmitter.vue"
describe("FormSubmitter", () => {
it("reveals a notification when submitted", async () => {
const wrapper = mount(FormSubmitter)
await wrapper.find("[data-username]").setValue("alice")
await wrapper.find("form").trigger("submit.prevent")
expect(wrapper.find(".message").text())
.toBe("Thank you for your submission, alice.")
})
})
This test is fairly self explanatory. We mount
the component, set the username and use the trigger
method vue-test-utils
provides to simulate user input. trigger
works on custom events, as well as events that use modifiers, like submit.prevent
, keydown.enter
, and so on.
Notice when we call setValue
and trigger
, we are using await
. This is why we had to mark the test as async
- so we can use await
.
setValue
and trigger
both, internally, return Vue.nextTick()
. As of vue-test-utils
beta 28, you need to call nextTick
to ensure Vue's reactivity system updates the DOM. By doing await setValue(...)
and await trigger(...)
, you are really just using a shorthand for:
wrapper.setValue(...)
await wrapper.vm.$nextTick() // "Wait for the DOM to update before continuing the test"
Sometimes you can get away without awaiting for nextTick
, but if you components start to get complex, you can hit a race condition and your assertion might run before Vue has updated the DOM. You can read more about this in the official vue-test-utils documentation.
The above test also follows the three steps of unit testing:
- arrange (set up for the test. In our case, we render the component).
- act (execute actions on the system)
- assert (ensure the actual result matches your expectations)
We separate each step with a newline as it makes tests more readable.
Run this test with yarn test:unit
. It should pass.
Trigger is very simple - use find
(for DOM elements) or findComponent
(for Vue components) to get the element you want to simulate some input, and call trigger
with the name of the event, and any modifiers.
# A real world example
Forms are usually submitted to some endpoint. Let's see how we might test this component with a different implementation of handleSubmit
. One common practice is to alias your HTTP library to Vue.prototype.$http
. This allows us to make an ajax request by simply calling this.$http.get(...)
. Learn more about this practice here.
Often the http library is, axios
, a popular HTTP client. In this case, our handleSubmit
would likely look something like this:
handleSubmitAsync() {
return this.$http.get("/api/v1/register", { username: this.username })
.then(() => {
// show success message, etc
})
.catch(() => {
// handle error
})
}
In this case, one technique is to mock this.$http
to create the desired testing environment. You can read about the global.mocks
mounting option here. Let's see a mock implementation of a http.get
method:
let url = ''
let data = ''
const mockHttp = {
get: (_url, _data) => {
return new Promise((resolve, reject) => {
url = _url
data = _data
resolve()
})
}
}
There are a few interesting things going on here:
- we create a
url
anddata
variable to save theurl
anddata
passed to$http.get
. This is useful to assert the request is hitting the correct endpoint, with correct payload. - after assigning the
url
anddata
arguments, we immediately resolve the Promise, to simulate a successful API response.
Before seeing the test, here is the new handleSubmitAsync
function:
methods: {
handleSubmitAsync() {
return this.$http.get("/api/v1/register", { username: this.username })
.then(() => {
this.submitted = true
})
.catch((e) => {
throw Error("Something went wrong", e)
})
}
}
Also, update <template>
to use the new handleSubmitAsync
method:
<template>
<div>
<form @submit.prevent="handleSubmitAsync">
<input v-model="username" data-username>
<input type="submit">
</form>
<!-- ... -->
</div>
</template>
Now, only the test.
# Mocking an ajax call
First, include the mock implementation of this.$http
at the top, before the describe
block:
let url = ''
let data = ''
const mockHttp = {
get: (_url, _data) => {
return new Promise((resolve, reject) => {
url = _url
data = _data
resolve()
})
}
}
Now, add the test, passing the mock $http
to the global.mocks
mounting option:
it("reveals a notification when submitted", () => {
const wrapper = mount(FormSubmitter, {
global: {
mocks: {
$http: mockHttp
}
}
})
wrapper.find("[data-username]").setValue("alice")
wrapper.find("form").trigger("submit.prevent")
expect(wrapper.find(".message").text())
.toBe("Thank you for your submission, alice.")
})
Now, instead of using whatever real http library is attached to Vue.prototype.$http
, the mock implementation will be used instead. This is good - we can control the environment of the test and get consistent results.
Running yarn test:unit
actually yields a failing test:
FAIL tests/unit/FormSubmitter.spec.js
● FormSubmitter › reveals a notification when submitted
[vue-test-utils]: find did not return .message, cannot call text() on empty Wrapper
What is happening is that the test is finishing before the promise returned by mockHttp
resolves. Again, we can make the test async like this:
it("reveals a notification when submitted", async () => {
// ...
})
Now we need to ensure the DOM has updated and all promises have resolved before the test continues. await wrapper.setValue(...)
is not always reliable here, either, because in this case we are not waiting for Vue to update the DOM, but an external dependency (our mocked HTTP client, in this case) to resolve.
One way to work around this is to use flush-promises, a simple Node.js module that will immediately resolve all pending promises. Install it with yarn add flush-promises
, and update the test as follows (we are also adding await wrapper.setValue(...)
for good measure):
import flushPromises from "flush-promises"
// ...
it("reveals a notification when submitted", async () => {
const wrapper = mount(FormSubmitter, {
global: {
mocks: {
$http: mockHttp
}
}
})
await wrapper.find("[data-username]").setValue("alice")
await wrapper.find("form").trigger("submit.prevent")
await flushPromises()
expect(wrapper.find(".message").text())
.toBe("Thank you for your submission, alice.")
})
Now the test passes. The source code for flush-promises
is only about 10 lines long, if you are interested in Node.js it is worth reading and understanding how it works.
We should also make sure the endpoint and payload are correct. Add two more assertions to the test:
// ...
expect(url).toBe("/api/v1/register")
expect(data).toEqual({ username: "alice" })
The test still passes.
# Conclusion
In this section, we saw how to:
- use
trigger
on events, even ones that use modifiers likeprevent
- use
setValue
to set a value of an<input>
usingv-model
- use
await
withtrigger
andsetValue
toawait Vue.nextTick
and ensure the DOM has updated - write tests using the three steps of unit testing
- mock a method attached to
Vue.prototype
using theglobal.mocks
mounting option - how to use
flush-promises
to immediately resolve all promises, a useful technique in unit testing
The source code for the test described on this page can be found here.