Lesson 22 / 25

Testing With Vitest and Vue Test Utils

Component tests that act like a user.

Mount, interact, assert

Vitest is a Vite-native test runner with a Jest-compatible API (describe, it, expect, vi.fn()), and Vue Test Utils (@vue/test-utils) mounts components in a simulated DOM such as jsdom or happy-dom. A component test mounts the component with props, finds elements, triggers events, waits for Vue to update (await trigger(...) returns a promise) and asserts on rendered text or emitted events via wrapper.emitted(). Test behaviour the user sees rather than internal details. Composables and Pinia stores can be tested as plain functions; @pinia/testing provides createTestingPinia for components that use stores. Use end-to-end tools such as Playwright or Cypress for full user journeys.

Tested, fast and ready to ship

Tests, performance tools and server rendering options take a Vue app from working to production-ready.

Four ideas: testing, performance, Nuxt and SSR, a release checklist.
Figure 8.1 — Testing, performance, SSR and checklist.

Testing a counter component

Props, interaction and emitted events.

// Counter.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('renders the initial count from props', () => {
    const wrapper = mount(Counter, { props: { start: 5 } })
    expect(wrapper.text()).toContain('Count: 5')
  })

  it('increments and emits change when clicked', async () => {
    const wrapper = mount(Counter, { props: { start: 0 } })
    await wrapper.find('button').trigger('click')

    expect(wrapper.text()).toContain('Count: 1')
    expect(wrapper.emitted('change')).toHaveLength(1)
    expect(wrapper.emitted('change')[0]).toEqual([1])
  })
})

Await DOM updates

Vue updates the DOM asynchronously. Await trigger, setValue or nextTick() before asserting, or your test will see the old output.

Quick check: Why is wrapper.find(...).trigger("click") awaited in a test?

  • Clicks are simulated with a delay of one second
  • trigger sends a network request
  • Vitest requires every call to be awaited
  • Vue updates the DOM asynchronously, and the promise resolves after the update
Answer

Vue updates the DOM asynchronously, and the promise resolves after the update — Awaiting lets the re-render finish.