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 hereSimulating user input - 图1.

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:

  1. <template>
  2. <div>
  3. <form @submit.prevent="handleSubmit">
  4. <input v-model="username" data-username>
  5. <input type="submit">
  6. </form>
  7. <div
  8. class="message"
  9. v-if="submitted"
  10. >
  11. Thank you for your submission, {{ username }}.
  12. </div>
  13. </div>
  14. </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:

  1. <script>
  2. export default {
  3. name: "FormSubmitter",
  4. data() {
  5. return {
  6. username: '',
  7. submitted: false
  8. }
  9. },
  10. methods: {
  11. handleSubmit() {
  12. this.submitted = true
  13. }
  14. }
  15. }
  16. </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.

  1. import { mount } from "@vue/test-utils"
  2. import FormSubmitter from "@/components/FormSubmitter.vue"
  3. describe("FormSubmitter", () => {
  4. it("reveals a notification when submitted", async () => {
  5. const wrapper = mount(FormSubmitter)
  6. await wrapper.find("[data-username]").setValue("alice")
  7. await wrapper.find("form").trigger("submit.prevent")
  8. expect(wrapper.find(".message").text())
  9. .toBe("Thank you for your submission, alice.")
  10. })
  11. })

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:

  1. wrapper.setValue(...)
  2. 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 documentationSimulating user input - 图2.

The above test also follows the three steps of unit testing:

  1. arrange (set up for the test. In our case, we render the component).
  2. act (execute actions on the system)
  3. 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 hereSimulating user input - 图3.

Often the http library is, axios, a popular HTTP client. In this case, our handleSubmit would likely look something like this:

  1. handleSubmitAsync() {
  2. return this.$http.get("/api/v1/register", { username: this.username })
  3. .then(() => {
  4. // show success message, etc
  5. })
  6. .catch(() => {
  7. // handle error
  8. })
  9. }

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 hereSimulating user input - 图4. Let’s see a mock implementation of a http.get method:

  1. let url = ''
  2. let data = ''
  3. const mockHttp = {
  4. get: (_url, _data) => {
  5. return new Promise((resolve, reject) => {
  6. url = _url
  7. data = _data
  8. resolve()
  9. })
  10. }
  11. }

There are a few interesting things going on here:

  • we create a url and data variable to save the url and data passed to $http.get. This is useful to assert the request is hitting the correct endpoint, with correct payload.
  • after assigning the url and data arguments, we immediately resolve the Promise, to simulate a successful API response.

Before seeing the test, here is the new handleSubmitAsync function:

  1. methods: {
  2. handleSubmitAsync() {
  3. return this.$http.get("/api/v1/register", { username: this.username })
  4. .then(() => {
  5. this.submitted = true
  6. })
  7. .catch((e) => {
  8. throw Error("Something went wrong", e)
  9. })
  10. }
  11. }

Also, update <template> to use the new handleSubmitAsync method:

  1. <template>
  2. <div>
  3. <form @submit.prevent="handleSubmitAsync">
  4. <input v-model="username" data-username>
  5. <input type="submit">
  6. </form>
  7. <!-- ... -->
  8. </div>
  9. </template>

Now, only the test.

Mocking an ajax call

First, include the mock implementation of this.$http at the top, before the describe block:

  1. let url = ''
  2. let data = ''
  3. const mockHttp = {
  4. get: (_url, _data) => {
  5. return new Promise((resolve, reject) => {
  6. url = _url
  7. data = _data
  8. resolve()
  9. })
  10. }
  11. }

Now, add the test, passing the mock $http to the global.mocks mounting option:

  1. it("reveals a notification when submitted", () => {
  2. const wrapper = mount(FormSubmitter, {
  3. global: {
  4. mocks: {
  5. $http: mockHttp
  6. }
  7. }
  8. })
  9. wrapper.find("[data-username]").setValue("alice")
  10. wrapper.find("form").trigger("submit.prevent")
  11. expect(wrapper.find(".message").text())
  12. .toBe("Thank you for your submission, alice.")
  13. })

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:

  1. FAIL tests/unit/FormSubmitter.spec.js
  2. FormSubmitter reveals a notification when submitted
  3. [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:

  1. it("reveals a notification when submitted", async () => {
  2. // ...
  3. })

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-promisesSimulating user input - 图5, 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):

  1. import flushPromises from "flush-promises"
  2. // ...
  3. it("reveals a notification when submitted", async () => {
  4. const wrapper = mount(FormSubmitter, {
  5. global: {
  6. mocks: {
  7. $http: mockHttp
  8. }
  9. }
  10. })
  11. await wrapper.find("[data-username]").setValue("alice")
  12. await wrapper.find("form").trigger("submit.prevent")
  13. await flushPromises()
  14. expect(wrapper.find(".message").text())
  15. .toBe("Thank you for your submission, alice.")
  16. })

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:

  1. // ...
  2. expect(url).toBe("/api/v1/register")
  3. 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 like prevent
  • use setValue to set a value of an <input> using v-model
  • use await with trigger and setValue to await Vue.nextTick ane ensure the DOM has updated
  • write tests using the three steps of unit testing
  • mock a method attached to Vue.prototype using the global.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 hereSimulating user input - 图6.