Skip to main content
Software EngineeringTutorials

Testing Vue Components: A Practical Approach

Stop writing brittle tests. Learn a pragmatic testing strategy for Vue components using Vitest and Vue Test Utils.

Mbeah Essilfie

Mbeah Essilfie

July 1, 2026 at 06:03 AM

9 min read 2075 views
Testing Vue Components: A Practical Approach

The Testing Philosophy

Don't test implementation details. Test behavior — what the user sees and does.

Setup

pnpm add -D vitest @vue/test-utils happy-dom
// 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,
  },
})

Testing a Button Component

<!-- components/AppButton.vue -->
<template>
  <button
    :disabled="loading"
    :class="{ 'opacity-50': loading }"
    @click="$emit('click')"
  >
    <span v-if="loading">Loading...</span>
    <slot v-else />
  </button>
</template>
import { mount } from '@vue/test-utils'
import AppButton from './AppButton.vue'

describe('AppButton', () => {
  it('renders slot content', () => {
    const wrapper = mount(AppButton, {
      slots: { default: 'Click me' },
    })
    expect(wrapper.text()).toBe('Click me')
  })

  it('shows loading state', () => {
    const wrapper = mount(AppButton, {
      props: { loading: true },
    })
    expect(wrapper.text()).toBe('Loading...')
    expect(wrapper.attributes('disabled')).toBeDefined()
  })

  it('emits click event', async () => {
    const wrapper = mount(AppButton, {
      slots: { default: 'Click' },
    })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })
})

Testing Composables

import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('increments count', () => {
    const { count, increment } = useCounter()
    expect(count.value).toBe(0)
    increment()
    expect(count.value).toBe(1)
  })
})
The testing pyramid for Vue: many composable/utility unit tests, some component integration tests, few E2E tests. Composables are the easiest to test thoroughly.

Testing Async Components

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

// Mock the fetch
vi.mock('~/composables/useApi', () => ({
  useApi: () => ({
    data: ref({ name: 'Mbeah', email: 'mbeah@test.com' }),
    loading: ref(false),
    error: ref(null),
  }),
}))

describe('UserProfile', () => {
  it('displays user data', async () => {
    const wrapper = mount(UserProfile)
    await flushPromises()
    expect(wrapper.text()).toContain('Mbeah')
  })
})

What NOT to Test

  • Third-party library internals
  • CSS classes (unless they represent state)
  • Exact DOM structure
  • Private methods or internal state

Test the contract: props in, events and rendered output out.

TypeScriptVueTesting
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
981