Vue
06 / 08

Testing Vue Applications

Testing Vue Applications

Comprehensive testing strategies for Vue applications using Vitest, Vue Test Utils, and end-to-end testing tools:

Vitest - Modern Testing Framework

Vitest is a blazing fast unit test framework powered by Vite. It provides Jest-compatible APIs with better performance.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
})

// Basic test
import { describe, it, expect } from 'vitest'
import { sum } from './utils'

describe('sum', () => {
  it('adds two numbers', () => {
    expect(sum(1, 2)).toBe(3)
  })
})

Vue Test Utils - Component Testing

Vue Test Utils is the official testing library for Vue components. It provides utilities to mount components and interact with them.

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

describe('Counter.vue', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, {
      props: {
        initialCount: 5,
      },
    })
    
    expect(wrapper.text()).toContain('Count: 5')
  })
  
  it('increments when button clicked', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('Count: 1')
    
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('Count: 2')
  })
  
  it('emits update event', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button').trigger('click')
    
    expect(wrapper.emitted()).toHaveProperty('update')
    expect(wrapper.emitted('update')).toHaveLength(1)
    expect(wrapper.emitted('update')[0]).toEqual([1])
  })
})

Testing with Pinia Stores

import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, it, expect } from 'vitest'
import TodoList from './TodoList.vue'
import { useTodoStore } from '@/stores/todos'

describe('TodoList with Pinia', () => {
  beforeEach(() => {
    // Create a fresh pinia instance for each test
    setActivePinia(createPinia())
  })
  
  it('displays todos from store', () => {
    const wrapper = mount(TodoList, {
      global: {
        plugins: [createPinia()],
      },
    })
    
    const store = useTodoStore()
    store.todos = [
      { id: 1, text: 'Test todo', completed: false },
    ]
    
    expect(wrapper.text()).toContain('Test todo')
  })
  
  it('adds todo when form submitted', async () => {
    const wrapper = mount(TodoList, {
      global: {
        plugins: [createPinia()],
      },
    })
    
    await wrapper.find('input').setValue('New todo')
    await wrapper.find('form').trigger('submit')
    
    const store = useTodoStore()
    expect(store.todos).toHaveLength(1)
    expect(store.todos[0].text).toBe('New todo')
  })
})

Testing Composables

import { describe, it, expect } from 'vitest'
import { useMouse } from './composables/useMouse'
import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'

describe('useMouse', () => {
  it('tracks mouse position', async () => {
    // Create a test component that uses the composable
    const TestComponent = defineComponent({
      setup() {
        const { x, y } = useMouse()
        return { x, y }
      },
      template: '<div>{{ x }}, {{ y }}</div>',
    })
    
    const wrapper = mount(TestComponent, {
      attachTo: document.body,
    })
    
    // Simulate mouse move
    const event = new MouseEvent('mousemove', {
      clientX: 100,
      clientY: 200,
    })
    window.dispatchEvent(event)
    
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.x).toBe(100)
    expect(wrapper.vm.y).toBe(200)
    
    wrapper.unmount()
  })
})

End-to-End Testing with Cypress

// cypress/e2e/todo-app.cy.js
describe('Vue Todo App', () => {
  beforeEach(() => {
    cy.visit('http://localhost:5173')
  })
  
  it('adds and completes todos', () => {
    // Add todo
    cy.get('[data-test="todo-input"]').type('Buy groceries')
    cy.get('[data-test="add-btn"]').click()
    
    // Verify added
    cy.contains('Buy groceries').should('be.visible')
    
    // Complete todo
    cy.get('[data-test="todo-checkbox"]').first().check()
    
    // Verify completed
    cy.get('[data-test="todo-item"]')
      .first()
      .should('have.class', 'completed')
  })
  
  it('filters todos', () => {
    // Add multiple todos
    const todos = ['Todo 1', 'Todo 2', 'Todo 3']
    todos.forEach(todo => {
      cy.get('[data-test="todo-input"]').type(todo)
      cy.get('[data-test="add-btn"]').click()
    })
    
    // Complete first
    cy.get('[data-test="todo-checkbox"]').first().check()
    
    // Filter completed
    cy.get('[data-test="filter-completed"]').click()
    cy.get('[data-test="todo-item"]').should('have.length', 1)
    
    // Filter active
    cy.get('[data-test="filter-active"]').click()
    cy.get('[data-test="todo-item"]').should('have.length', 2)
  })
})

Testing Best Practices

  • Test behavior, not implementation details

  • Use data-test attributes for stable selectors

  • Mock external dependencies (APIs, stores)

  • Test user interactions with await and $nextTick

  • Keep tests isolated and independent

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free