Testing Vue Applications
Testing Stack
The recommended testing stack for Vue 3 is:
- Vitest — fast unit test runner (Vite-native, Jest-compatible API)
- Vue Test Utils (VTU) — official Vue component testing library
- @testing-library/vue — user-centric testing (alternative to VTU)
- Playwright / Cypress — end-to-end testing
# Install testing dependencies
npm install -D vitest @vue/test-utils jsdom @vitest/coverage-v8
# vite.config.js — add test config
# export default defineConfig({
# plugins: [vue()],
# test: {
# environment: 'jsdom',
# globals: true,
# }
# })
# package.json scripts
# "test": "vitest",
# "test:run": "vitest run",
# "test:coverage": "vitest run --coverage"
# Run tests
npm run test # watch mode
npm run test:run # single run
npm run test:coverage # with coverage report
// Counter.test.js — testing a Counter component
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter', () => {
let wrapper
beforeEach(() => {
wrapper = mount(Counter, {
props: { initialCount: 0 }
})
})
it('renders initial count', () => {
expect(wrapper.text()).toContain('0')
})
it('increments count when + button clicked', async () => {
const button = wrapper.find('[data-testid="increment"]')
await button.trigger('click')
expect(wrapper.text()).toContain('1')
})
it('decrements count when - button clicked', async () => {
// First increment to 1
await wrapper.find('[data-testid="increment"]').trigger('click')
// Then decrement
await wrapper.find('[data-testid="decrement"]').trigger('click')
expect(wrapper.text()).toContain('0')
})
it('resets count when reset button clicked', async () => {
await wrapper.find('[data-testid="increment"]').trigger('click')
await wrapper.find('[data-testid="increment"]').trigger('click')
await wrapper.find('[data-testid="reset"]').trigger('click')
expect(wrapper.text()).toContain('0')
})
it('emits update event when count changes', async () => {
await wrapper.find('[data-testid="increment"]').trigger('click')
expect(wrapper.emitted('update')).toBeTruthy()
expect(wrapper.emitted('update')[0]).toEqual([1])
})
it('does not go below 0', async () => {
await wrapper.find('[data-testid="decrement"]').trigger('click')
expect(wrapper.text()).toContain('0')
})
it('accepts initialCount prop', () => {
const w = mount(Counter, { props: { initialCount: 10 } })
expect(w.text()).toContain('10')
})
})
// UserList.test.js — testing async component with API
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import UserList from '@/components/UserList.vue'
// Mock fetch
const mockUsers = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
]
describe('UserList', () => {
beforeEach(() => {
// Reset mocks before each test
vi.restoreAllMocks()
})
it('shows loading state initially', () => {
global.fetch = vi.fn(() => new Promise(() => {})) // never resolves
const wrapper = mount(UserList)
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(true)
})
it('renders users after successful fetch', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockUsers)
})
)
const wrapper = mount(UserList)
await flushPromises() // wait for all promises to resolve
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(false)
expect(wrapper.findAll('[data-testid="user-item"]')).toHaveLength(2)
expect(wrapper.text()).toContain('Alice')
expect(wrapper.text()).toContain('Bob')
})
it('shows error message on fetch failure', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('Network error')))
const wrapper = mount(UserList)
await flushPromises()
expect(wrapper.find('[data-testid="error"]').exists()).toBe(true)
expect(wrapper.text()).toContain('Network error')
})
it('filters users by search query', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) })
)
const wrapper = mount(UserList)
await flushPromises()
const searchInput = wrapper.find('[data-testid="search"]')
await searchInput.setValue('Alice')
expect(wrapper.findAll('[data-testid="user-item"]')).toHaveLength(1)
expect(wrapper.text()).toContain('Alice')
expect(wrapper.text()).not.toContain('Bob')
})
it('deletes user when delete button clicked', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) })
)
const wrapper = mount(UserList)
await flushPromises()
const deleteButtons = wrapper.findAll('[data-testid="delete-user"]')
await deleteButtons[0].trigger('click')
expect(wrapper.findAll('[data-testid="user-item"]')).toHaveLength(1)
expect(wrapper.text()).not.toContain('Alice')
})
})
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.