Vue.js 3 Unit Testing Components Tutorial

In this Vue tutorial we learn how to test specific Vue features with Jest.

We cover testing components and their children, elements in components, computed properties, props and native and custom events.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI with the Jest Unit Testing package installed .

The project should look similar to the following.

Example: project
project-name/
├── public/
├── src/
|
├── tests/
|   ├── unit/
|   |   └── example.spec.js
|
└── jest.config.js

How to test a component

Now that we know how testing works, we can start learning how it applies to Vue.

A component needs to be mounted before it can be tested. Vue’s Test Utilities provides us with the mount and shallowMount methods to do that.

Both methods will return a wrapper object containing the Vue component, as well as some helper methods for testing.

But, there are some important differences between them.

  • mount will mount the component with any child components included.
  • shallowMount will replace child components with a stub.

A stub is basically a fake object that acts as a stand-in for a real object. In Vue, a component stub looks as follows.

Example: component stub
<vuecomponent-stub></vuecomponent-stub>

In most cases we should try to use mount first. If there are any complications, we can try using shallowMount.

Both methods are imported into our test script from ‘@vue/test-utils’ and they take the component we want to mount as an argument.

As an example, let’s change our root App component to output a greeting message to the page. Then test if that greeting message contains the correct words.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
export default {
  data() {
    return {
      greeting: 'Hello World'
    }
  }
}
</script>

As mentioned, we need to import the mounting method and the component we want to mount. In this case, the component is the root App component.

We’ll use describe it syntax and store the mounted component object in a constant called wrapper .

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders text: Hello World', () => {

    // mount App.vue and return
    // component object
    const wrapper = mount(App)
  })
})

Once the component is mounted, we can use the helper functions in our assertions. One of those helpers is the text method, which returns the text content of an element.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders text: Hello World', () => {

    const wrapper = mount(App)

    // test if text inside first
    // element in App component
    // is 'Hello World'
    expect(wrapper.text()).toBe('Hello World')
  })
})

If we run the test, it should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Renders text: Hello World (21ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.631s
Ran all test suites.

Although we will use many of the helpers in the following lessons, the official VTU API Documentation contains a full list of everything that you can use.

How to test if an element exists in a component's template

Sometimes we will need to test if an element in a component’s template exists, before we can test it. To do that, the Vue Test Utilities gives us the find and exists methods.

Because find is essentially a wrapper for Javascript’s querySelector , we can use the same syntax.

Syntax:
// element only
expect(wrapper.find('element').exists()).toBeTruthy()

// element with class
expect(wrapper.find('.class_name').exists()).toBeTruthy()

// element with id
expect(wrapper.find('#id_name').exists()).toBeTruthy()

// etc.

In this case we used Jest’s toBeTruthy matcher, but you can also use toBe(true) .

Example:
expect(wrapper.find("[attribute='value']").exists()).toBe(true)

The root App component from our previous example displays the greeting in a paragraph.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
export default {
  data() {
    return {
      greeting: 'Hello World'
    }
  }
}
</script>

So in our test we can find the “p” element.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Check if paragraph exists', () => {
    const wrapper = mount(App)

    // find the 'p' element in App.vue's template
    expect(wrapper.find('p').exists()).toBeTruthy()
  })
})

Because the paragraph exists, the test will pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Check if paragraph exists (27ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.746s
Ran all test suites.

Let’s say we only want to test the greeting message if the paragraph exists. All we need to do, is use our assertion as a condition in an if statement.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders greeting Hello World', () => {
    const wrapper = mount(App)

    if (expect(wrapper.find('p').exists()).toBeTruthy()) {

      // only test greeting message if 'p' element exists
      expect(wrapper.text()).toBe('Hello World')
    }
  })
})

Because the paragraph exists and the greeting is “Hello World”, the test will pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Renders greeting Hello World (28ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.268s
Ran all test suites.

tip You can also use the get method instead of find . The only difference between the two is that get will throw an error if the element cannot be found.

note The find method should only be used to find elements in a component. To find the component itself, we should use the getComponent , findComponent and findAllComponents utility methods.

How to find a child component

We can test if a component contains a child with the Vue Test Utilities findComponent method.

The method takes the component we want to find as argument. We have several options to find the component.

Syntax: getComponent
const wrapper = mount(parent_component)

// pass the imported component directly
wrapper.findComponent(child_component)

// search by name
wrapper.findComponent({ name: 'child_component' })

// by querySelector
wrapper.findComponent('child_component')
wrapper.findComponent('#child_component_id')
wrapper.findComponent('.child_component_class')
// etc.

If the component is found, it will return a wrapper for it.

As an example, let’s create a /src/components/GreetingMessage.vue component with a simple greeting in the template.

Example: project
project-name/
├── public/
├── src/
|   ├── components/
|   |   └── GreetingMessage.vue
|
├── tests/
|   ├── unit/
|   |   └── example.spec.js
|
└── jest.config.js
Example: src/components/GreetingMessage.vue
<template>
  <p>Hello World</p>
</template>

Then we’ll import it into the root App component and use it in the template.

Example: src/App.vue
<template>
  <greeting-message />
</template>

<script>
import GreetingMessage from './components/GreetingMessage.vue'

export default {
  components: { GreetingMessage }
}
</script>

In our test, we’ll start by finding the GreetingMessage component in App.vue and store its wrapper in a constant called greet .

Then we’ll use the new wrapper to find the paragraph element and test if the greeting message is “Hello World”, like we did in the previous section above.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders greeting Hello World in child', () => {
    const wrapper = mount(App)

    // find the 'GreetingMessage' child
    // in the root App component
    const greet = wrapper.getComponent({ name: 'GreetingMessage' })

    // find the paragraph inside it and
    // test if the greeting message matches
    if (expect(greet.find('p').exists()).toBe(true)) {
      expect(greet.text()).toBe('Hello World')
    }
  })
})

Because the component exists in App.vue , the test should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Renders greeting Hello World in child (29ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.755s
Ran all test suites.

tip You can also use the getComponent method instead of findComponent . The only difference between the two is that getComponent will throw an error if the element cannot be found.

How to test computed properties

There are two ways we can test computed properties.

  1. mount . Using the mounted component’s wrapper to test the computed property’s output in the template.
  2. call . Calling the computed property directly with local values.

For our example, we’ll use a computed property in the root App component that combines first and last name data properties.

Example: src/App.vue
<template>
  <p>Hello {{ fullName }}</p>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John', lastName: 'Doe'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}
</script>

Our test file will be the example one we’ve been using throughout the lesson: tests/unit/example.spec.js .

Testing computed property output in the template

The template outputs the text “Hello John Doe”. We don’t care about the “Hello” part, we just want to know if the computed property is doing its job.

So instead of testing for an exact value with toBe , we’ll check if “John Doe” exists somewhere in the text with toContain .

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Contains computed value John Doe', () => {
    const wrapper = mount(App)

    // test if the template contains
    // the text 'John Doe' somewhere
    expect(wrapper.text()).toContain('John Doe')
  })
})

If we run the example, the test should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Contains computed value John Doe (20ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.177s
Ran all test suites.

Calling a computed property with test values

We can test a computed property without mounting the component. To do that, we access computed.property_name on the imported component, then use the call method.

Syntax: call
component_name.computed.computed_property_name.call()

Because the component isn’t mounted, this doesn’t exist. So the computed property can’t use the values we specified in the component.

We will need to define our own values in the test and pass them to the call method.

Example: local test values
component_name.computed.computed_property_name.call({
  // define test values that
  // match the ones used in
  // the computed property
  name: 'John'
})

Our example uses the two data properties firstName and lastName , so we can just define values for them.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Contains computed value Jane Doe', () => {
    // define test values that
    // match the ones used in
    // the computed property
    const testValues = {
      firstName: 'Jane',
      lastName: 'Doe'
    }

    // call the computed property directly
    // on App.vue with the local test values
    expect(App.computed.fullName.call(testValues)).toContain('Jane Doe')
  })
})

note The values that we define locally inside the test will be used instead of the data properties defined in the App component.

When we run the test, it should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Contains computed value Jane Doe (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.78s
Ran all test suites.

How to test props

There are two ways we can test props.

  1. mount . Using local test values defined in the mounting options.
  2. props . Testing the prop’s value directly with the props utility method.

Testing with local values defined in the mounting options

The mount and shallowMount methods can take a second argument, which is an object that contains the mounting options.

We can use the mounting options to specify any props we want to test, with values.

Syntax: props
describe('Component', () => {

  it('Test description', () => {

    const wrapper = mount(Component, {
      props: {
        prop_name: value_to_test
      }
    })

    // assertion
  })
})

To keep our example simple, we’ll add a greeting prop to our root App component and output it in the template.

Example: src/App.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
export default {
  props: {
    greeting: { type: String }
  }
}
</script>

Right now the prop doesn’t receive a value and if we run the example in the browser, it won’t display anything.

But that’s okay, because we can give our prop any value we want in the test.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders message Hello World', () => {

    const wrapper = mount(App, {
      props: {
        // give prop a value
        greeting: 'Hello World'
      }
    })

    // test the value from above
    expect(wrapper.text()).toBe('Hello World')
  })
})

If we run the test above, it should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Renders message Hello World (21ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.44s
Ran all test suites.

How to test a prop with the props method

We don’t have to specify our own value like above, we can test for the exact value we pass to a prop. To do that, we use the props method on the child component’s wrapper.

Syntax:
// test prop value on child
expect(child_wrapper.props('prop_name')).toBe(prop_value)

Because we’re checking the prop’s value directly, it doesn’t have to be specified in the mounting options anymore.

As an example, let’s define a greeting prop in our GreetingMessage component.

Example: src/components/GreetingMessage.vue
<template>
  <p>{{ greeting }}</p>
</template>

<script>
export default {
  props: {
    greeting: String
  }
}
</script>

Then we’ll import and use it in the root App component, passing a value to the prop.

Example: src/App.vue
<template>
  <greeting-message greeting="Hello World" />
</template>

<script>
import GreetingMessage from './components/GreetingMessage'

export default {
  components: { GreetingMessage }
}
</script>

In our test, we’ll remove the mounting options, get the GreetingMessage child component and use the props method in our assertion.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('Renders message Hello World', () => {
    // parent
    const wrapper = mount(App)
    // child
    const greet = wrapper.getComponent({
      name: 'GreetingMessage'
    })

    // test value passed in 'App' component's template
    expect(greet.props('greeting')).toBe('Hello World')
  })
})

Because the messages are the same, the test should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ Renders message Hello World (21ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.009s
Ran all test suites.

How to test native DOM events

A native DOM event is how the browser tells Javascript that something interesting happened. For example, clicking on a button triggers a click event, submitting a form triggers a submit event, etc.

Vue handles these events with the v-on directive and its @ shorthand.

Example: @click
<button @click="count++">Increment</button>

The Vue Test Utilities gives us the trigger method to manually trigger such an event and test its affect.

The method is used on an element and takes the event we want to trigger as a string argument.

Syntax: trigger
wrapper.find('element').trigger('event')

As an example, we’ll create a simple click counter in the root App component.

Example: src/App.vue
<template>
  <button @click="increment">Clicks: {{ count }}</button>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

In the test, we’ll trigger the click event on the button and test that it incremented from 0 to 1.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {
  it('increments counter when button is clicked', () => {

    const wrapper = mount(App)

    // find and click on button
    wrapper.find('button').trigger('click')

    // test output from 0 to 1
    expect(wrapper.text()).toBe('Clicks: 1')
   })
})

But if we run the test, it fails with the following output.

Output:
FAIL  tests/unit/example.spec.js (2.798s)
 App.vue
   × increments counter when button is clicked (80ms)

 ● App.vue › increments counter when button is clicked

   expect(received).toBe(expected) // Object.is equality

   Expected: "Clicks: 1"
   Received: "Clicks: 0"

     14 |
     15 |     // Test output from 0 to 1
   > 16 |     expect(wrapper.text()).toBe('Clicks: 1')
        |                            ^
     17 |    })
     18 | })
     19 |

     at Object.<anonymous> (tests/unit/example.spec.js:16:28)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.945s
Ran all test suites.

So the value didn’t increment. Our first instinct would be to assume that the trigger method didn’t work, or we made a typo. But something else is actually going on here.

When we trigger an event, Vue doesn’t automatically wait for the DOM to update. In our case, the click event was triggered, but Vue didn’t wait for count to show 1.

So all we need to do is to tell Vue to wait for the update by marking the trigger with await . To use await , we need to mark the it/test with async .

Example: async await
it('description', async () => {

  await wrapper.find('element').trigger('event')
})

tip A good way to know when to use await is when the statement needs to update the DOM.

Let’s change our example to wait for the update.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it('increments counter when button is clicked', async () => {

    const wrapper = mount(App)

    // find and click on button and
    // wait for the DOM to update
    await wrapper.find('button').trigger('click')

    // test output from 0 to 1
    expect(wrapper.text()).toBe('Clicks: 1')
   })
})

This time when we run the test, it will pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ increments counter when button is clicked (35ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.437s, estimated 12s
Ran all test suites.

How to test custom emitted events

A custom emitted event is how we communicate from a child component to a parent component. We do so by passing data to the special $emit instance variable.

Example: $emit
this.$emit('event_name', value-to-send)

The Vue Test Utilities gives us the emitted method to help test these events.

The method returns an object with two keys, the event name and the type of event it was.

The event name’s value is an array that contains another array of the emitted values.

Example: emitted return
{
  event_name: [ [value] ],
  event_type: [ [ [type] ] ]
}

If an event is triggered multiple times, it will be tracked in the outer array.

Example: multiple triggers
{
  event_name: [
    [trigger_1],
    [trigger_2]
  ],
  event_type: [ [ [type] ] ]
}

If an event passes multiple values, it will be tracked in the inner arrays.

Example: multiple values
{
  event_name: [
    [trigger_1_value_1, trigger_1_value_2],
    [trigger_2_value_1, trigger_2_value_2]
  ],
  event_type: [ [ [type] ] ]
}

To access the value, we chain the event name to the emitted method, and use the indexer two levels deep.

Example: array indexer
//      outer array -v  v- inner array
emitted().event_name[0][0].toBe(['value'])

As an example, we’ll use the same counter as in the previous section. We’ll emit a count event from the method with the count number as value.

Example: src/App.vue
<template>
  <button @click="increment">Clicks: {{ count }}</button>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
      // emit event called 'count'
      // with a payload of whatever
      // the count is
      this.$emit('count', this.count)
    }
  }
}
</script>

In the test, we’ll show the emitted return in a console log first to help the demonstration.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it("emit 'count' event with correct payload", async () => {

    const wrapper = mount(App)

    // trigger the emit
    await wrapper.find('button').trigger('click')

    console.log(wrapper.emitted())
   })
})

When we run the test, it should show the following.

Output:
console.log tests/unit/example.spec.js:13
    { count: [ [ 1 ] ], click: [ [ [MouseEvent] ] ] }

We’re looking for the value of 1, which is in the first value of the first array. In other words [0][0] .

So our assertion would chain the count[0][0] to the emitted method and test the output.

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it("emit 'count' event with correct payload", async () => {

    const wrapper = mount(App)

    // trigger the emit
    await wrapper.find('button').trigger('click')
    console.log(wrapper.emitted())

    // first value in first inner array
    expect(wrapper.emitted().count[0][0]).toBe(1)
   })
})

Because the count will increment to 1, the test will pass when we run it.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ emit 'count' event with correct payload (65ms)

 console.log tests/unit/example.spec.js:12
   { count: [ [ 1 ] ], click: [ [ [MouseEvent] ] ] }

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.752s
Ran all test suites.

Next, let’s trigger the event a second time and test that the count increased to 2.

We’re looking for the first value in the second inner array, in other words [1][0] .

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it("emit 'count' event twice with correct payload", async () => {

    const wrapper = mount(App)

    // trigger the emit twice
    await wrapper.find('button').trigger('click')
    await wrapper.find('button').trigger('click')
    console.log(wrapper.emitted())

    // first value in first inner array
    expect(wrapper.emitted().count[0][0]).toBe(1)
    // first value in second inner array
    expect(wrapper.emitted().count[1][0]).toBe(2)
   })
})

Because the count increased from 1 to 2 after the second trigger, the test should pass.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ emit 'count' event twice with correct payload (68ms)

 console.log tests/unit/example.spec.js:13
   {
     count: [ [ 1 ], [ 2 ] ],
     click: [ [ [MouseEvent] ], [ [MouseEvent] ] ]
   }

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.61s
Ran all test suites.

As a final demonstration, let’s add a second value to the event.

Example: src/App.vue
<template>
  <button @click="increment">Clicks: {{ count }}</button>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
      // emit event called 'count'
      // with two values
      this.$emit('count', this.count, 'Hello')
    }
  }
}
</script>

This time, we’re looking for the second value in both inner arrays. In other words [0][1] and [1][1] .

Example: tests/unit/example.spec.js
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('App.vue', () => {

  it("emit 'count' event twice with correct payloads", async () => {

    const wrapper = mount(App)

    // trigger the emit twice
    await wrapper.find('button').trigger('click')
    await wrapper.find('button').trigger('click')
    console.log(wrapper.emitted())

    // first value in first inner array
    expect(wrapper.emitted().count[0][0]).toBe(1)
    // first value in second inner array
    expect(wrapper.emitted().count[1][0]).toBe(2)

    // second value in both inner arrays
    expect(wrapper.emitted().count[0][1]).toBe('Hello')
    expect(wrapper.emitted().count[1][1]).toBe('Hello')
   })
})

The test should pass with the following output.

Output:
PASS  tests/unit/example.spec.js
 App.vue
   √ emit 'count' event twice with correct payloads (74ms)

 console.log tests/unit/example.spec.js:13
   {
     count: [ [ 1, 'Hello' ], [ 2, 'Hello' ] ],
     click: [ [ [MouseEvent] ], [ [MouseEvent] ] ]
   }

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.746s
Ran all test suites.

tip The arrays can be a little confusing sometimes. If you get stuck, console log the emitted method.

Further Reading

For more information on the topics covered in this lesson, please see the relevant sections below.