Testing
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Vue tests split into unit tests (plain functions and composables), component tests (a component mounted in isolation), and end-to-end tests (the real app in a real browser). The Vue guide’s Testing page frames the choices; this page is a practical tour.
The testing pyramid
Write many fast unit tests, a moderate number of component tests, and a few slow end-to-end tests. Unit and component tests run in milliseconds under Node and catch logic and rendering bugs; end-to-end tests are the only ones that exercise routing, real network calls, and the browser, so keep them focused on critical user journeys.
Vitest
Vitest is the recommended runner for a Vite project: it reuses your vite.config
(aliases, plugins, @vitejs/plugin-vue) so components compile the same way in tests as in the app.
create-vue can add it; manually:
npm i -D vitest @vue/test-utils happy-dom
// vitest.config.js
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
environment: 'happy-dom', // a DOM implementation for component tests
globals: true, // describe/it/expect without imports
},
}))
A test file:
import { describe, it, expect } from 'vitest'
import { add } from '@/utils/math'
describe('add', () => {
it('sums two numbers', () => {
expect(add(2, 3)).toBe(5)
})
})
Component testing with Vue Test Utils
Vue Test Utils mounts a component and returns a wrapper with a query and interaction API.
import { mount } from '@vue/test-utils'
import Stepper from '@/components/Stepper.vue'
it('increments and emits the new value', async () => {
const wrapper = mount(Stepper, {
props: { modelValue: 1, max: 3 },
})
await wrapper.find('[data-test="inc"]').trigger('click')
expect(wrapper.text()).toContain('2')
expect(wrapper.emitted('update:modelValue')).toEqual([[2]])
})
Key wrapper methods:
-
mountrenders the component and its children;shallowMountstubs child components so the test is isolated to this component’s own template. -
find/findAlltake a CSS selector and return DOM wrappers;findComponent/findAllComponentstake a component and return component wrappers. -
trigger('click'),trigger('keydown.enter')dispatch DOM events;setValue('text')sets an input’s value and firesinput. -
emitted()records every custom event —emitted('submit')is an array of argument arrays, one per emission. -
props(),text(),html(),classes(),isVisible()inspect the render.
Waiting for updates
Vue applies DOM updates asynchronously. trigger, setValue, and setProps return promises you can
await; for state changed directly, await nextTick(); for pending microtasks such as resolved fetches,
await flushPromises():
import { flushPromises, mount } from '@vue/test-utils'
const wrapper = mount(UserCard, { props: { id: 7 } })
await flushPromises() // let the mocked API promise settle
expect(wrapper.text()).toContain('Ada Lovelace')
Stubs, mocks, and slots
Provide global.stubs to replace heavy children, global.mocks for injected globals, and slots to pass
slot content:
const wrapper = mount(Layout, {
slots: {
default: '<p>body</p>',
header: HeaderStub,
},
global: {
stubs: { RouterLink: true },
mocks: { $t: (key) => key },
},
})
Mock modules (an API client, vue-router) with vi.mock('@/api') from Vitest.
Testing composables
A composable that uses no lifecycle hooks or injection can be called directly in a test:
import { useCounter } from '@/composables/useCounter'
it('counts', () => {
const { count, inc } = useCounter()
inc()
expect(count.value).toBe(1)
})
If it calls onMounted, provide/inject, or getCurrentInstance, mount it inside a throwaway host
component so it runs in a real component context:
import { mount } from '@vue/test-utils'
function withSetup(composable) {
let result
const Comp = { setup() { result = composable(); return () => {} } }
const wrapper = mount(Comp)
return { result, wrapper }
}
See Testing composables.
Testing with Router and Pinia
Install a real router with in-memory history and await router.isReady(); install a fresh Pinia per test.
@pinia/testing provides createTestingPinia(), which stubs actions by default so you can assert they were
called:
import { createTestingPinia } from '@pinia/testing'
import { createRouter, createMemoryHistory } from 'vue-router'
const router = createRouter({ history: createMemoryHistory(), routes })
const wrapper = mount(App, {
global: {
plugins: [router, createTestingPinia()],
},
})
await router.isReady()
Snapshot testing
expect(wrapper.html()).toMatchSnapshot() records rendered markup and flags future diffs. Useful for
stable presentational components; avoid it for components that change often, where snapshots become noise
that gets blindly updated.
Component testing vs. end-to-end
Component tests mount one component under a simulated DOM — fast, but not a real browser. End-to-end tests drive the built app in an actual browser and are the place to verify navigation, focus management, real CSS, and integration with a backend. Cypress and Playwright also offer a component testing mode that mounts a Vue component in a real browser, a middle ground when jsdom/happy-dom fidelity is not enough.
End-to-end with Cypress or Playwright
Cypress and Playwright both start the dev server, open a browser, and script user interactions. They auto-wait for elements and let you intercept HTTP so tests stay deterministic:
// Playwright
import { test, expect } from '@playwright/test'
test('adds a todo', async ({ page }) => {
await page.route('**/api/todos', (route) =>
route.fulfill({ json: [] }),
)
await page.goto('/')
await page.getByPlaceholder('What needs doing?').fill('Write tests')
await page.getByRole('button', { name: 'Add' }).click()
await expect(page.getByText('Write tests')).toBeVisible()
})
// Cypress
it('adds a todo', () => {
cy.intercept('GET', '/api/todos', [])
cy.visit('/')
cy.get('[data-test="new-todo"]').type('Write tests{enter}')
cy.contains('Write tests').should('be.visible')
})
create-vue can scaffold either one.
See also
-
Vue Test Utils and Vitest — the full APIs.
-
Tooling and Project Setup — the Vite config Vitest inherits.
-
State Management and Routing — the plugins wired in above.