Vue Test Utils: Complete Guide to Testing Vue Components

Vue Test Utils: Complete Guide to Testing Vue Components

Vue Test Utils is the official testing library for Vue 3 components, providing utilities to mount components in isolation and assert their behavior. This guide covers everything from basic mounting to advanced slot and prop testing patterns used in production Vue 3 projects.

Key Takeaways

Use mount for integration and shallowMount for isolation. mount renders the full component tree, while shallowMount stubs child components — choose based on what you're testing.

wrapper.find() returns a DOMWrapper — check .exists() before asserting. Forgetting .exists() causes cryptic errors when elements are conditionally rendered.

await wrapper.trigger('click') is async — always await it. Vue batches DOM updates asynchronously; skipping await means asserting against stale DOM.

Pass props via mountOptions.props, not via template strings. The props mount option is type-safe and mirrors how the component receives data in production.

Test behavior, not implementation. Assert what the user sees and does — text content, visible elements, emitted events — not internal state or method calls.

Vue Test Utils is the official testing companion for Vue 3. Paired with Vitest or Jest, it lets you mount components in a simulated browser environment and make assertions about their output. This guide walks through the full API with practical TypeScript examples.

Installation and Setup

Install the core dependencies:

npm install -D @vue/test-utils vitest jsdom @vitejs/plugin-vue

Configure Vitest in vite.config.ts:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
  },
})

mount vs shallowMount

The first decision you make for every test is how to mount the component.

mount renders the complete component tree — the component under test plus all its children. Use it when child component behavior matters to the test.

shallowMount stubs all child components with placeholder elements. Use it when you want to test a component in isolation, without caring whether its children render correctly.

import { mount, shallowMount } from '@vue/test-utils'
import ParentComponent from './ParentComponent.vue'

// Full render — ChildComponent actually runs
const fullWrapper = mount(ParentComponent)

// Isolated render — ChildComponent is stubbed
const shallowWrapper = shallowMount(ParentComponent)

A practical rule: start with shallowMount. If you find yourself asserting on output that lives inside a child component, switch to mount.

Passing Props

Props are passed through the props mount option:

import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'

test('displays user name and role', () => {
  const wrapper = mount(UserCard, {
    props: {
      name: 'Alice',
      role: 'admin',
      isActive: true,
    },
  })

  expect(wrapper.text()).toContain('Alice')
  expect(wrapper.text()).toContain('admin')
  expect(wrapper.find('.status-badge').classes()).toContain('status-badge--active')
})

For components with required props, omitting them in tests causes Vue warnings. Always provide all required props, even if their value does not matter for the specific assertion.

Testing Prop Changes

Vue Test Utils respects Vue's reactivity. You can test that a component reacts correctly when props change:

test('shows inactive state when isActive is false', async () => {
  const wrapper = mount(UserCard, {
    props: { name: 'Bob', role: 'viewer', isActive: true },
  })

  await wrapper.setProps({ isActive: false })

  expect(wrapper.find('.status-badge').classes()).toContain('status-badge--inactive')
})

Finding Elements

Vue Test Utils provides several methods to find elements in the rendered output.

wrapper.find()

Returns the first matching element as a DOMWrapper. Always check .exists() before asserting on a conditionally rendered element:

test('shows error message when validation fails', async () => {
  const wrapper = mount(LoginForm)

  await wrapper.find('input[type="email"]').setValue('')
  await wrapper.find('button[type="submit"]').trigger('click')

  const error = wrapper.find('.field-error')
  expect(error.exists()).toBe(true)
  expect(error.text()).toBe('Email is required')
})

wrapper.findAll()

Returns all matching elements as an array of DOMWrapper instances:

test('renders correct number of list items', () => {
  const wrapper = mount(TodoList, {
    props: {
      items: ['Buy milk', 'Walk dog', 'Write tests'],
    },
  })

  const items = wrapper.findAll('.todo-item')
  expect(items).toHaveLength(3)
  expect(items[0].text()).toBe('Buy milk')
})

wrapper.findComponent()

Finds a child component instance — useful when you need to assert on component-level props or emitted events from children:

import ChildModal from './ChildModal.vue'

test('passes correct data to modal', () => {
  const wrapper = mount(ParentPage)
  const modal = wrapper.findComponent(ChildModal)

  expect(modal.props('title')).toBe('Confirm deletion')
})

Triggering Events

DOM events are triggered with wrapper.trigger(). It always returns a Promise because Vue batches DOM updates — await it before asserting:

test('increments counter on button click', async () => {
  const wrapper = mount(Counter)

  expect(wrapper.find('.count').text()).toBe('0')

  await wrapper.find('button.increment').trigger('click')

  expect(wrapper.find('.count').text()).toBe('1')
})

Form Inputs

Use setValue() for input, select, and textarea elements. It sets the value and dispatches both input and change events:

test('enables submit button when form is valid', async () => {
  const wrapper = mount(ContactForm)

  const submitButton = wrapper.find('button[type="submit"]')
  expect(submitButton.attributes('disabled')).toBeDefined()

  await wrapper.find('input[name="name"]').setValue('Alice')
  await wrapper.find('input[name="email"]').setValue('alice@example.com')
  await wrapper.find('textarea[name="message"]').setValue('Hello there')

  expect(submitButton.attributes('disabled')).toBeUndefined()
})

Keyboard Events

Pass event options as the second argument to trigger():

test('closes modal on Escape key', async () => {
  const wrapper = mount(Modal, { props: { isOpen: true } })

  await wrapper.trigger('keydown', { key: 'Escape' })

  expect(wrapper.find('.modal-overlay').exists()).toBe(false)
})

Testing Emitted Events

Components communicate with parents via emitted events. Assert on wrapper.emitted():

test('emits submit event with form data', async () => {
  const wrapper = mount(LoginForm)

  await wrapper.find('input[name="email"]').setValue('user@example.com')
  await wrapper.find('input[name="password"]').setValue('secret123')
  await wrapper.find('form').trigger('submit')

  expect(wrapper.emitted('submit')).toHaveLength(1)
  expect(wrapper.emitted('submit')![0]).toEqual([
    { email: 'user@example.com', password: 'secret123' },
  ])
})

wrapper.emitted() returns an object where keys are event names and values are arrays of argument arrays — one inner array per emission.

Testing Slots

Slots allow parent components to inject content. Test them by providing slot content in mount options:

test('renders default slot content', () => {
  const wrapper = mount(Card, {
    slots: {
      default: '<p class="slot-content">Card body text</p>',
    },
  })

  expect(wrapper.find('.slot-content').exists()).toBe(true)
  expect(wrapper.find('.slot-content').text()).toBe('Card body text')
})

Named Slots

test('renders header and footer slots', () => {
  const wrapper = mount(Layout, {
    slots: {
      header: '<h1 class="page-title">Dashboard</h1>',
      default: '<p>Main content</p>',
      footer: '<span class="footer-text">2026</span>',
    },
  })

  expect(wrapper.find('.page-title').text()).toBe('Dashboard')
  expect(wrapper.find('.footer-text').text()).toBe('2026')
})

Scoped Slots

When a component exposes data back to the slot, use a render function:

test('exposes item data through scoped slot', () => {
  const wrapper = mount(DataList, {
    props: { items: [{ id: 1, name: 'First item' }] },
    slots: {
      item: ({ item }: { item: { id: number; name: string } }) =>
        `<div class="item-name">${item.name}</div>`,
    },
  })

  expect(wrapper.find('.item-name').text()).toBe('First item')
})

Global Configuration

Avoid repeating global setup (plugins, components, directives) in every test. Use a custom mount helper:

// test-utils.ts
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import router from '@/router'

export function mountWithPlugins(component: any, options = {}) {
  return mount(component, {
    global: {
      plugins: [createPinia(), router],
    },
    ...options,
  })
}

Testing Async Components

Components that fetch data on mount need special handling. Use flushPromises() to resolve all pending promises before asserting:

import { flushPromises } from '@vue/test-utils'
import { vi } from 'vitest'
import UserProfile from './UserProfile.vue'
import * as api from '@/api'

test('displays fetched user data', async () => {
  vi.spyOn(api, 'fetchUser').mockResolvedValue({
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
  })

  const wrapper = mount(UserProfile, { props: { userId: 1 } })

  expect(wrapper.find('.loading-spinner').exists()).toBe(true)

  await flushPromises()

  expect(wrapper.find('.loading-spinner').exists()).toBe(false)
  expect(wrapper.find('.user-name').text()).toBe('Alice')
})

Common Pitfalls

Always await DOM updates. Any time you interact with the component (trigger, setValue, setProps), await the result before asserting.

Do not test implementation details. Avoid asserting on component data properties directly (wrapper.vm.someInternalValue). Test the DOM output — what a user would actually see.

Use data-testid attributes sparingly. They are useful when there is no semantic selector available, but prefer semantic HTML (roles, labels, text) when possible.

Clean up global mocks. Use afterEach(() => vi.restoreAllMocks()) to prevent mock bleed between tests.

Vue Test Utils gives you everything you need to test Vue 3 components thoroughly. Think like a user — interact with the component the way a real user would, and assert on what they would see.

Read more

Start now free