Test Vue components from the user’s perspective — verify rendered output and interactions, not implementation details.

Setup with Vitest

  npm install -D vitest @vue/test-utils jsdom @testing-library/vue
  

vite.config.js:

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

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

package.json:

  { "scripts": { "test": "vitest" } }
  

Basic Component Test

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

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain('Count: 0');
  });

  it('increments on button click', async () => {
    const wrapper = mount(Counter);
    await wrapper.find('button').trigger('click');
    expect(wrapper.text()).toContain('Count: 1');
  });
});
  

Counter component:

  <script setup>
import { ref } from 'vue';
const count = ref(0);
</script>

<template>
  <button @click="count++">Count: {{ count }}</button>
</template>
  

Testing Props and Emits

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

it('displays user name from props', () => {
  const wrapper = mount(UserCard, {
    props: { name: 'Alice', email: '[email protected]' },
  });
  expect(wrapper.text()).toContain('Alice');
});

it('emits delete event', async () => {
  const wrapper = mount(UserCard, { props: { name: 'Bob' } });
  await wrapper.find('[data-testid="delete"]').trigger('click');
  expect(wrapper.emitted('delete')).toHaveLength(1);
});
  

Testing Async Components

  import { mount, flushPromises } from '@vue/test-utils';
import PostList from './PostList.vue';

it('loads and displays posts', async () => {
  global.fetch = vi.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve([{ id: 1, title: 'Hello' }]),
    })
  );

  const wrapper = mount(PostList);
  expect(wrapper.text()).toContain('Loading');

  await flushPromises();
  expect(wrapper.text()).toContain('Hello');
});
  

Testing with Pinia

  import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import CartSummary from './CartSummary.vue';

it('shows cart total', () => {
  setActivePinia(createPinia());
  const wrapper = mount(CartSummary);
  expect(wrapper.text()).toContain('Total: $0');
});
  

What NOT to Test

  • Internal ref variable names
  • Third-party library internals

Run Tests

  npm test           # Watch mode
npm test -- --run  # Single run
npm test -- --coverage
  

Write tests that confirm your UI works for users.