Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to run unit tests in Vue

Testing Vue components ensures your application works correctly and prevents regressions when refactoring or adding features. With 12 years of experience building Vue applications since 2014 and as the creator of CoreUI, I’ve established comprehensive testing strategies for production applications. The most efficient approach is using Vitest with Vue Test Utils, which provides fast test execution, Vue 3 Composition API support, and seamless integration with Vite. This combination offers instant feedback during development and reliable test coverage for components, composables, and business logic.

Use Vitest with Vue Test Utils to run fast, reliable unit tests for Vue 3 components.

npm install -D vitest @vue/test-utils happy-dom
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'happy-dom'
  }
})
// package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}

This configuration sets up Vitest with Vue support using happy-dom as the DOM environment. The globals: true option allows using describe, it, and expect without importing them in every test file. The test scripts provide different modes: continuous testing with watch mode, interactive UI for debugging, and coverage reports for tracking test completeness.

Writing Your First Component Test

Start with a simple component test to verify rendering and basic functionality.

// components/Counter.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click='increment'>Increment</button>
    <button @click='decrement'>Decrement</button>
  </div>
</template>

<script>
export default {
  setup() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    const decrement = () => {
      count.value--
    }

    return {
      count,
      increment,
      decrement
    }
  }
}
</script>
// components/Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

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

  it('increments count when button clicked', async () => {
    const wrapper = mount(Counter)
    const button = wrapper.find('button')

    await button.trigger('click')

    expect(wrapper.text()).toContain('Count: 1')
  })

  it('decrements count when second button clicked', async () => {
    const wrapper = mount(Counter)
    const buttons = wrapper.findAll('button')

    await buttons[1].trigger('click')

    expect(wrapper.text()).toContain('Count: -1')
  })
})

The mount() function creates a component instance for testing. The wrapper provides methods to query elements, trigger events, and inspect the component’s state. The await keyword before trigger() ensures Vue’s reactivity system has updated the DOM before making assertions. This test verifies the component renders correctly and responds to user interactions.

Testing Props and Emits

Components often receive data via props and communicate with parents via events.

// components/UserCard.vue
<template>
  <div class='user-card'>
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <button @click='handleEdit'>Edit</button>
    <button @click='handleDelete'>Delete</button>
  </div>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    }
  },
  emits: ['edit', 'delete'],
  setup(props, { emit }) {
    const handleEdit = () => {
      emit('edit', props.user.id)
    }

    const handleDelete = () => {
      emit('delete', props.user.id)
    }

    return {
      handleEdit,
      handleDelete
    }
  }
}
</script>
// components/UserCard.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import UserCard from './UserCard.vue'

describe('UserCard', () => {
  const mockUser = {
    id: 1,
    name: 'John Doe',
    email: '[email protected]'
  }

  it('renders user information', () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    })

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('[email protected]')
  })

  it('emits edit event with user id', async () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    })

    await wrapper.find('button').trigger('click')

    expect(wrapper.emitted()).toHaveProperty('edit')
    expect(wrapper.emitted('edit')[0]).toEqual([1])
  })

  it('emits delete event with user id', async () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    })

    const buttons = wrapper.findAll('button')
    await buttons[1].trigger('click')

    expect(wrapper.emitted('delete')[0]).toEqual([1])
  })
})

Props are passed to the component via the props option in mount(). The emitted() method returns all events emitted by the component, allowing you to verify the event name and payload. This ensures components correctly communicate with their parents without needing to mount the parent component.

Mocking Composables

Composables often contain side effects like API calls that should be mocked in tests.

// components/UserProfile.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { ref } from 'vue'
import UserProfile from './UserProfile.vue'
import * as userComposable from '@/composables/useUser'

describe('UserProfile', () => {
  it('displays user data when loaded', async () => {
    const mockUseUser = {
      user: ref({ name: 'John Doe', email: '[email protected]' }),
      loading: ref(false),
      error: ref(null),
      fetchUser: vi.fn()
    }

    vi.spyOn(userComposable, 'useUser').mockReturnValue(mockUseUser)

    const wrapper = mount(UserProfile)

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('[email protected]')
  })

  it('displays loading state', () => {
    const mockUseUser = {
      user: ref(null),
      loading: ref(true),
      error: ref(null),
      fetchUser: vi.fn()
    }

    vi.spyOn(userComposable, 'useUser').mockReturnValue(mockUseUser)

    const wrapper = mount(UserProfile)

    expect(wrapper.text()).toContain('Loading')
  })
})

The vi.spyOn() method intercepts calls to the composable and returns a mock implementation. This allows testing different states (loading, success, error) without making real API calls. The vi.fn() creates a mock function that tracks calls and arguments.

Testing Async Behavior

Components often perform asynchronous operations that require special handling in tests.

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

describe('AsyncButton', () => {
  it('shows loading state during async operation', async () => {
    const wrapper = mount(AsyncButton)
    const button = wrapper.find('button')

    expect(button.text()).toBe('Submit')
    expect(button.attributes('disabled')).toBeUndefined()

    await button.trigger('click')

    expect(button.text()).toBe('Loading...')
    expect(button.attributes('disabled')).toBeDefined()

    await flushPromises()
    await wrapper.vm.$nextTick()

    expect(button.text()).toBe('Submit')
    expect(button.attributes('disabled')).toBeUndefined()
  })

  it('prevents multiple clicks during loading', async () => {
    const wrapper = mount(AsyncButton)
    const button = wrapper.find('button')

    await button.trigger('click')
    await button.trigger('click')

    expect(button.attributes('disabled')).toBeDefined()
  })
})

The flushPromises() function waits for all pending promises to resolve, ensuring async operations complete before assertions. The $nextTick() method waits for Vue’s reactivity system to update the DOM. Together, they ensure the component’s state and UI are synchronized before verifying the results.

Testing Slots and Dynamic Content

Slots allow components to accept dynamic content from parents.

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

describe('Card', () => {
  it('renders default slot content', () => {
    const wrapper = mount(Card, {
      slots: {
        default: 'Custom body content'
      }
    })

    expect(wrapper.text()).toContain('Custom body content')
  })

  it('renders named slot content', () => {
    const wrapper = mount(Card, {
      slots: {
        header: 'Custom Header',
        default: 'Custom Body',
        footer: 'Custom Footer'
      }
    })

    expect(wrapper.text()).toContain('Custom Header')
    expect(wrapper.text()).toContain('Custom Body')
    expect(wrapper.text()).toContain('Custom Footer')
  })
})

Slots are provided via the slots option in mount(). The default slot fills the unnamed slot, while named slots use their specific names. This allows testing how components render different slot configurations without creating wrapper components.

Best Practice Note

This is the same testing approach we use in CoreUI Vue components to ensure reliable behavior across all our UI elements. For more advanced testing patterns, check out how to test Vue components with Jest and how to test Vue components with Vue Test Utils. Always test user interactions, not implementation details—focus on what users see and do, not internal state or method names. When building applications with CoreUI for Vue, leverage the provided test utilities and component mocks to speed up your testing workflow and maintain high code quality.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author