Vitest + Vue Test Utils
Testing Vue components: Vitest + Vue Test Utils, mounting, props, events, async, and the patterns for fast green tests.
Vue — testing
EXAMPLE
# Install:
npm install -D vitest @vue/test-utils happy-dom @vitest/coverage-v8
# vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: { environment: 'happy-dom', globals: true },
});
# ===== A first test =====
# Counter.vue
<script setup>
import { ref } from 'vue';
const props = defineProps(['start']);
const emit = defineEmits(['change']);
const count = ref(props.start ?? 0);
const inc = () => { count.value++; emit('change', count.value); };
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
# Counter.test.ts
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('renders start prop', () => {
const wrapper = mount(Counter, { props: { start: 5 } });
expect(wrapper.text()).toBe('5');
});
test('emits change on click', async () => {
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toBe('1');
expect(wrapper.emitted('change')[0]).toEqual([1]);
});
# ===== Mount options =====
mount(Component, {
props: { ... },
attrs: { ... },
slots: { default: 'Hello' },
global: {
plugins: [router, pinia],
stubs: { ChildComponent: true },
mocks: { $t: (key) => key },
},
});
# shallowMount: stubs all child components (great for isolation).
# ===== Async + waitUntil =====
import { nextTick, flushPromises } from '@vue/test-utils';
await wrapper.setProps({ value: 'x' });
await nextTick();
await flushPromises();
# ===== User-event style queries =====
wrapper.find('button').trigger('click');
wrapper.find('input').setValue('hello');
wrapper.find('select').setValue('option-1');
wrapper.find('[data-testid=submit]').trigger('click');
# ===== Pinia store tests =====
import { createPinia, setActivePinia } from 'pinia';
beforeEach(() => setActivePinia(createPinia()));
test('store action', () => {
const store = useCounterStore();
store.inc();
expect(store.count).toBe(1);
});
# ===== Composable tests =====
import { useCounter } from './useCounter';
test('useCounter increments', () => {
const { value, inc } = useCounter(0);
inc();
expect(value.value).toBe(1);
});
# ===== Snapshot tests =====
expect(wrapper.html()).toMatchSnapshot();
# Use sparingly; brittle when UI iterates fast.
# ===== Patterns =====
# - mount over shallowMount when integration matters; shallowMount for unit
# - data-testid attributes for stable selectors
# - Test BEHAVIOUR (clicks + emits + content) not internals
# - Mock router / fetch / Pinia where you need isolation
# ===== Pitfalls =====
# - Selecting by CSS classes (refactor breaks tests)
# - Missing await on trigger() / setProps() (Vue re-renders asynchronously)
# - Snapshot tests on everything -> noisy updates
# - Slow tests; check imports and global plugin overhead
Why it matters
Vitest + Vue Test Utils + happy-dom is the modern stack. Mount components, trigger events, await re-renders, assert text + emitted events. Use data-testid for stable selectors, mock router / pinia per test, prefer behaviour over snapshots. Fast green tests are the lever for confident refactors.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { mount } from '@vue/test-utils';
import { test, expect } from 'vitest';
test('renders', () => {
const w = mount(Greet, { props: { name: 'Ada' } });
expect(w.text()).toContain('Ada');
});
Try it Yourself »
Discussion
Loading…