Comprehensive unit testing guide for Vue 3 + Vite projects using Vitest and Vue Test Utils...
Generate comprehensive, production-ready unit tests for Vue 3 + Vite projects using Vitest framework. Follow industry best practices for testing Vue components, composables, Pinia stores, and TypeScript utilities with proper isolation, mocking, and edge case coverage.
Primary Stack:
Import pattern:
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mount, shallowMount } from '@vue/test-utils';
Follow this systematic approach for all testing tasks:
Before writing any tests:
Only proceed to writing tests after full code understanding.
Plan test coverage:
For Vue components, identify:
For composables, identify:
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('ModuleName or ComponentName', () => {
// Top-level test variables
let mockDependency: MockType;
beforeEach(() => {
// Reset state before each test
mockDependency = createMockDependency();
});
afterEach(() => {
// Cleanup after each test
vi.clearAllMocks();
});
describe('method or feature name', () => {
it('should handle happy path scenario', () => {
// Arrange: Set up test data and mocks
const input = { /* test data */ };
// Act: Execute the code under test
const result = functionUnderTest(input);
// Assert: Verify expected outcomes
expect(result).toBe(expectedValue);
});
it('should handle error case', async () => {
// Arrange
mockDependency.method.mockRejectedValue(new Error('test error'));
// Act & Assert
await expect(functionUnderTest()).rejects.toThrow('test error');
});
it('should handle edge case: empty input', () => {
// Test edge cases
expect(functionUnderTest([])).toEqual([]);
});
});
});
Always structure individual tests using AAA:
it('should calculate total price correctly', () => {
// Arrange: Set up test data
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
];
// Act: Execute the function
const total = calculateTotal(items);
// Assert: Verify the result
expect(total).toBe(250);
});
Decide between mount vs shallowMount:
mount() for integration testing with child componentsshallowMount() for isolated unit testing (stubs child components)import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('should render with props', () => {
const wrapper = mount(MyComponent, {
props: {
title: 'Test Title',
count: 5
}
});
expect(wrapper.find('h1').text()).toBe('Test Title');
expect(wrapper.find('.count').text()).toBe('5');
});
it('should emit event on button click', async () => {
const wrapper = mount(MyComponent);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('submit')).toBeTruthy();
expect(wrapper.emitted('submit')[0]).toEqual([{ data: 'value' }]);
});
it('should handle v-model binding', async () => {
const wrapper = mount(MyComponent, {
props: {
modelValue: 'initial'
}
});
await wrapper.find('input').setValue('updated');
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['updated']);
});
});
See references/component-testing.md for complete component testing patterns including slots, provide/inject, and async components.
import { composableUnderTest } from './useFeature';
describe('useFeature composable', () => {
it('should initialize with default state', () => {
const { state, count } = composableUnderTest();
expect(state.value).toBe('idle');
expect(count.value).toBe(0);
});
it('should update reactive state', () => {
const { increment, count } = composableUnderTest();
increment();
expect(count.value).toBe(1);
});
it('should handle async operations', async () => {
const { fetchData, data, loading } = composableUnderTest();
expect(loading.value).toBe(false);
const promise = fetchData();
expect(loading.value).toBe(true);
await promise;
expect(loading.value).toBe(false);
expect(data.value).toBeDefined();
});
});
See references/composables-testing.md for advanced composable testing patterns including side effects and cleanup.
import { setActivePinia, createPinia } from 'pinia';
import { useMyStore } from './myStore';
describe('myStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('should initialize with default state', () => {
const store = useMyStore();
expect(store.items).toEqual([]);
expect(store.loading).toBe(false);
});
it('should add item to store', () => {
const store = useMyStore();
const newItem = { id: 1, name: 'Test' };
store.addItem(newItem);
expect(store.items).toContainEqual(newItem);
});
it('should handle async actions', async () => {
const store = useMyStore();
await store.fetchItems();
expect(store.loading).toBe(false);
expect(store.items.length).toBeGreaterThan(0);
});
});
See references/store-testing.md for Pinia store testing patterns including getters, mutations, and actions.
// Mock API calls
vi.mock('@/api/users', () => ({
fetchUsers: vi.fn(),
createUser: vi.fn()
}));
// Mock composables
vi.mock('@/composables/useAuth', () => ({
useAuth: vi.fn(() => ({
user: { id: 1, name: 'Test User' },
isAuthenticated: true,
login: vi.fn(),
logout: vi.fn()
}))
}));
// Mock Vue Router
const mockRouter = {
push: vi.fn(),
replace: vi.fn()
};
const wrapper = mount(Component, {
global: {
mocks: {
$router: mockRouter
}
}
});
import { vi } from 'vitest';
describe('setTimeout behavior', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should execute callback after delay', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledOnce();
});
});
beforeEach to reset statevi.clearAllMocks() or vi.resetAllMocks()// ✅ Good: Clear and descriptive
it('should display error message when API request fails', () => {});
// ❌ Bad: Vague and unclear
it('should work', () => {});
// ❌ Bad: Contains loops and conditions
it('should validate all items', () => {
for (const item of items) {
if (item.type === 'special') {
expect(validate(item)).toBe(true);
}
}
});
// ✅ Good: Simple and direct
it('should validate special item', () => {
const specialItem = { type: 'special', value: 100 };
expect(validate(specialItem)).toBe(true);
});
it('should validate normal item', () => {
const normalItem = { type: 'normal', value: 50 };
expect(validate(normalItem)).toBe(true);
});
// ✅ Properly handle async operations
it('should fetch data successfully', async () => {
const result = await fetchData();
expect(result).toBeDefined();
});
// ✅ Use resolves/rejects for promises
await expect(fetchData()).resolves.toEqual(expectedData);
await expect(failingOperation()).rejects.toThrow('Error message');
When generating tests, always provide:
For detailed examples and advanced patterns:
references/component-testing.md - Comprehensive component testing patterns (slots, teleport, provide/inject, async components)references/composables-testing.md - Advanced composable testing (side effects, watchers, cleanup)references/store-testing.md - Pinia store testing patterns (getters, actions, state management)