iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…