Testing Pinia Stores
Comprehensive examples and patterns for testing Pinia stores with various testing frameworks, including unit tests, integration tests, and end-to-end testing strategies.
Features
- 🧪 Unit testing with Vitest/Jest
- 🔗 Integration testing patterns
- 🎭 End-to-end testing with Playwright
- 🎯 Mocking strategies and best practices
- 📊 Coverage reporting and analysis
- 🔄 Testing async operations and side effects
- 🛡️ Error handling and edge case testing
- 🎨 Testing store composition and dependencies
- 📱 Component integration testing
- 🔧 Custom testing utilities and helpers
Testing Setup
Vitest Configuration
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'src/main.ts'
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
testTimeout: 10000,
hookTimeout: 10000
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@stores': resolve(__dirname, 'src/stores'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils')
}
}
})
Test Setup File
typescript
// src/test/setup.ts
import { vi } from 'vitest'
import { config } from '@vue/test-utils'
import { createPinia } from 'pinia'
// Global test setup
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks()
// Clear localStorage
localStorage.clear()
sessionStorage.clear()
// Reset fetch mock
global.fetch = vi.fn()
})
// Vue Test Utils global config
config.global.plugins = [createPinia()]
// Mock IntersectionObserver
global.IntersectionObserver = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn()
}))
// Mock ResizeObserver
global.ResizeObserver = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn()
}))
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}))
})
Testing Utilities
Store Testing Helpers
typescript
// src/test/store-utils.ts
import { createPinia, setActivePinia, type Pinia } from 'pinia'
import { beforeEach, afterEach, vi } from 'vitest'
import type { App } from 'vue'
import { createApp } from 'vue'
export interface StoreTestContext {
pinia: Pinia
app: App
}
export function setupStoreTest(): StoreTestContext {
let pinia: Pinia
let app: App
beforeEach(() => {
app = createApp({})
pinia = createPinia()
app.use(pinia)
setActivePinia(pinia)
})
afterEach(() => {
vi.clearAllMocks()
app.unmount()
})
return {
get pinia() { return pinia },
get app() { return app }
}
}
export function createMockApi<T extends Record<string, any>>(methods: T): T {
const mock = {} as T
for (const [key, value] of Object.entries(methods)) {
if (typeof value === 'function') {
mock[key as keyof T] = vi.fn(value) as T[keyof T]
} else {
mock[key as keyof T] = value
}
}
return mock
}
export async function waitForStoreAction(
fn: () => Promise<any>,
timeout = 5000
): Promise<void> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Store action timed out after ${timeout}ms`)), timeout)
})
try {
await Promise.race([fn(), timeoutPromise])
} catch (error) {
if (error instanceof Error && error.message.includes('timed out')) {
throw error
}
// Re-throw the original error from the store action
throw error
}
}
export function createMockResponse<T>(data: T, options: {
status?: number
statusText?: string
headers?: Record<string, string>
} = {}): Response {
const {
status = 200,
statusText = 'OK',
headers = { 'Content-Type': 'application/json' }
} = options
return new Response(JSON.stringify(data), {
status,
statusText,
headers
})
}
export function mockFetch(responses: Array<{
url: string | RegExp
response: any
status?: number
delay?: number
}>): void {
global.fetch = vi.fn().mockImplementation(async (url: string) => {
const match = responses.find(r =>
typeof r.url === 'string' ? r.url === url : r.url.test(url)
)
if (!match) {
throw new Error(`No mock response found for URL: ${url}`)
}
if (match.delay) {
await new Promise(resolve => setTimeout(resolve, match.delay))
}
return createMockResponse(match.response, { status: match.status })
})
}
Component Testing Helpers
typescript
// src/test/component-utils.ts
import { mount, type VueWrapper } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import type { Component } from 'vue'
export interface ComponentTestOptions {
props?: Record<string, any>
slots?: Record<string, any>
global?: {
plugins?: any[]
mocks?: Record<string, any>
stubs?: Record<string, any>
}
}
export function mountWithPinia(
component: Component,
options: ComponentTestOptions = {}
): VueWrapper {
const pinia = createPinia()
setActivePinia(pinia)
return mount(component, {
...options,
global: {
plugins: [pinia, ...(options.global?.plugins || [])],
...options.global
}
})
}
export async function waitForAsyncComponent(
wrapper: VueWrapper,
timeout = 1000
): Promise<void> {
await wrapper.vm.$nextTick()
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Component async operation timed out after ${timeout}ms`))
}, timeout)
const checkComplete = () => {
if (!wrapper.vm.$el.querySelector('[data-testid="loading"]')) {
clearTimeout(timer)
resolve()
} else {
setTimeout(checkComplete, 10)
}
}
checkComplete()
})
}
Unit Testing Examples
Testing Basic Store Operations
typescript
// src/stores/__tests__/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useCounterStore } from '@stores/counter'
import { setupStoreTest } from '@/test/store-utils'
describe('Counter Store', () => {
setupStoreTest()
it('should initialize with default state', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
expect(store.doubleCount).toBe(0)
expect(store.isEven).toBe(true)
})
it('should increment count', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
expect(store.doubleCount).toBe(2)
expect(store.isEven).toBe(false)
})
it('should decrement count', () => {
const store = useCounterStore()
store.increment() // count = 1
store.decrement() // count = 0
expect(store.count).toBe(0)
expect(store.isEven).toBe(true)
})
it('should increment by custom amount', () => {
const store = useCounterStore()
store.incrementBy(5)
expect(store.count).toBe(5)
expect(store.doubleCount).toBe(10)
})
it('should reset count', () => {
const store = useCounterStore()
store.incrementBy(10)
store.reset()
expect(store.count).toBe(0)
expect(store.doubleCount).toBe(0)
})
})
Testing Async Operations
typescript
// src/stores/__tests__/todos.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useTodosStore } from '@stores/todos'
import { setupStoreTest, createMockApi, waitForStoreAction } from '@/test/store-utils'
import type { Todo } from '@/types'
const mockTodosApi = createMockApi({
fetchTodos: vi.fn(),
createTodo: vi.fn(),
updateTodo: vi.fn(),
deleteTodo: vi.fn()
})
vi.mock('@/api/todos', () => ({
todosApi: mockTodosApi
}))
describe('Todos Store', () => {
setupStoreTest()
const mockTodos: Todo[] = [
{
id: '1',
title: 'Test Todo 1',
completed: false,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: '2',
title: 'Test Todo 2',
completed: true,
createdAt: new Date(),
updatedAt: new Date()
}
]
beforeEach(() => {
vi.clearAllMocks()
})
it('should fetch todos successfully', async () => {
const store = useTodosStore()
mockTodosApi.fetchTodos.mockResolvedValue({
data: mockTodos,
message: 'Success',
success: true,
timestamp: new Date()
})
await waitForStoreAction(() => store.fetchTodos())
expect(store.todos).toEqual(mockTodos)
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
expect(mockTodosApi.fetchTodos).toHaveBeenCalledOnce()
})
it('should handle fetch todos error', async () => {
const store = useTodosStore()
const error = new Error('Network error')
mockTodosApi.fetchTodos.mockRejectedValue(error)
await expect(store.fetchTodos()).rejects.toThrow('Network error')
expect(store.todos).toEqual([])
expect(store.isLoading).toBe(false)
expect(store.hasError).toBe(true)
expect(store.error).toBe('Network error')
})
it('should create todo with optimistic update', async () => {
const store = useTodosStore()
const newTodo = {
title: 'New Todo',
completed: false
}
const createdTodo: Todo = {
id: '3',
...newTodo,
createdAt: new Date(),
updatedAt: new Date()
}
mockTodosApi.createTodo.mockResolvedValue({
data: createdTodo,
message: 'Created',
success: true,
timestamp: new Date()
})
// Check optimistic update
const createPromise = store.createTodo(newTodo)
// Should have temporary todo immediately
expect(store.todos).toHaveLength(1)
expect(store.todos[0].title).toBe(newTodo.title)
expect(store.todos[0].id).toMatch(/^temp-/)
await waitForStoreAction(() => createPromise)
// Should replace temporary todo with real one
expect(store.todos).toHaveLength(1)
expect(store.todos[0]).toEqual(createdTodo)
expect(mockTodosApi.createTodo).toHaveBeenCalledWith(newTodo)
})
it('should rollback optimistic update on create error', async () => {
const store = useTodosStore()
const newTodo = {
title: 'New Todo',
completed: false
}
mockTodosApi.createTodo.mockRejectedValue(new Error('Create failed'))
const createPromise = store.createTodo(newTodo)
// Should have temporary todo
expect(store.todos).toHaveLength(1)
await expect(createPromise).rejects.toThrow('Create failed')
// Should rollback to empty state
expect(store.todos).toHaveLength(0)
expect(store.hasError).toBe(true)
})
it('should update todo', async () => {
const store = useTodosStore()
// Setup initial state
store.$patch({ todos: [...mockTodos] })
const updates = { completed: true }
const updatedTodo = { ...mockTodos[0], ...updates, updatedAt: new Date() }
mockTodosApi.updateTodo.mockResolvedValue({
data: updatedTodo,
message: 'Updated',
success: true,
timestamp: new Date()
})
await waitForStoreAction(() => store.updateTodo('1', updates))
expect(store.todos[0]).toEqual(updatedTodo)
expect(mockTodosApi.updateTodo).toHaveBeenCalledWith('1', updates)
})
it('should delete todo', async () => {
const store = useTodosStore()
// Setup initial state
store.$patch({ todos: [...mockTodos] })
mockTodosApi.deleteTodo.mockResolvedValue({
data: null,
message: 'Deleted',
success: true,
timestamp: new Date()
})
await waitForStoreAction(() => store.deleteTodo('1'))
expect(store.todos).toHaveLength(1)
expect(store.todos[0].id).toBe('2')
expect(mockTodosApi.deleteTodo).toHaveBeenCalledWith('1')
})
it('should filter todos correctly', () => {
const store = useTodosStore()
store.$patch({ todos: [...mockTodos] })
expect(store.completedTodos).toHaveLength(1)
expect(store.completedTodos[0].id).toBe('2')
expect(store.pendingTodos).toHaveLength(1)
expect(store.pendingTodos[0].id).toBe('1')
expect(store.completionRate).toBe(0.5)
})
})
Testing Store Composition
typescript
// src/stores/__tests__/user-profile.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUserStore } from '@stores/user'
import { useProfileStore } from '@stores/profile'
import { setupStoreTest, createMockApi } from '@/test/store-utils'
const mockUserApi = createMockApi({
login: vi.fn(),
logout: vi.fn()
})
const mockProfileApi = createMockApi({
getProfile: vi.fn(),
updateProfile: vi.fn()
})
vi.mock('@/api/user', () => ({ userApi: mockUserApi }))
vi.mock('@/api/profile', () => ({ profileApi: mockProfileApi }))
describe('Store Composition - User Profile', () => {
setupStoreTest()
it('should sync profile data when user logs in', async () => {
const userStore = useUserStore()
const profileStore = useProfileStore()
const mockUser = {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
}
const mockProfile = {
userId: '1',
bio: 'Test bio',
avatar: 'avatar.jpg',
preferences: {
theme: 'dark',
language: 'en'
}
}
mockUserApi.login.mockResolvedValue({
data: { user: mockUser, token: 'token', expiresAt: new Date().toISOString() },
message: 'Success',
success: true,
timestamp: new Date()
})
mockProfileApi.getProfile.mockResolvedValue({
data: mockProfile,
message: 'Success',
success: true,
timestamp: new Date()
})
await userStore.login({ email: 'test@example.com', password: 'password' })
// Profile should be automatically fetched after login
expect(profileStore.profile).toEqual(mockProfile)
expect(mockProfileApi.getProfile).toHaveBeenCalledWith('1')
})
it('should clear profile data when user logs out', async () => {
const userStore = useUserStore()
const profileStore = useProfileStore()
// Setup initial state
profileStore.$patch({
profile: {
userId: '1',
bio: 'Test bio',
avatar: 'avatar.jpg',
preferences: { theme: 'dark', language: 'en' }
}
})
await userStore.logout()
expect(profileStore.profile).toBeNull()
})
it('should handle profile update with user store sync', async () => {
const userStore = useUserStore()
const profileStore = useProfileStore()
// Setup initial state
userStore.$patch({
user: {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
},
isAuthenticated: true
})
const updatedProfile = {
userId: '1',
bio: 'Updated bio',
avatar: 'new-avatar.jpg',
preferences: { theme: 'light', language: 'es' }
}
mockProfileApi.updateProfile.mockResolvedValue({
data: updatedProfile,
message: 'Updated',
success: true,
timestamp: new Date()
})
await profileStore.updateProfile({
bio: 'Updated bio',
avatar: 'new-avatar.jpg'
})
expect(profileStore.profile).toEqual(updatedProfile)
expect(userStore.user?.preferences).toEqual(updatedProfile.preferences)
})
})
Integration Testing
Testing Component-Store Integration
typescript
// src/components/__tests__/TodoList.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mountWithPinia, waitForAsyncComponent } from '@/test/component-utils'
import { createMockApi, mockFetch } from '@/test/store-utils'
import TodoList from '@/components/TodoList.vue'
import { useTodosStore } from '@stores/todos'
const mockTodosApi = createMockApi({
fetchTodos: vi.fn(),
createTodo: vi.fn(),
updateTodo: vi.fn(),
deleteTodo: vi.fn()
})
vi.mock('@/api/todos', () => ({
todosApi: mockTodosApi
}))
describe('TodoList Component Integration', () => {
const mockTodos = [
{
id: '1',
title: 'Test Todo 1',
completed: false,
createdAt: new Date(),
updatedAt: new Date()
},
{
id: '2',
title: 'Test Todo 2',
completed: true,
createdAt: new Date(),
updatedAt: new Date()
}
]
beforeEach(() => {
vi.clearAllMocks()
})
it('should render todos from store', async () => {
mockTodosApi.fetchTodos.mockResolvedValue({
data: mockTodos,
message: 'Success',
success: true,
timestamp: new Date()
})
const wrapper = mountWithPinia(TodoList)
await waitForAsyncComponent(wrapper)
const todoItems = wrapper.findAll('[data-testid="todo-item"]')
expect(todoItems).toHaveLength(2)
expect(todoItems[0].text()).toContain('Test Todo 1')
expect(todoItems[1].text()).toContain('Test Todo 2')
})
it('should create new todo when form is submitted', async () => {
const newTodo = {
id: '3',
title: 'New Todo',
completed: false,
createdAt: new Date(),
updatedAt: new Date()
}
mockTodosApi.fetchTodos.mockResolvedValue({
data: [],
message: 'Success',
success: true,
timestamp: new Date()
})
mockTodosApi.createTodo.mockResolvedValue({
data: newTodo,
message: 'Created',
success: true,
timestamp: new Date()
})
const wrapper = mountWithPinia(TodoList)
await waitForAsyncComponent(wrapper)
const input = wrapper.find('[data-testid="todo-input"]')
const form = wrapper.find('[data-testid="todo-form"]')
await input.setValue('New Todo')
await form.trigger('submit')
expect(mockTodosApi.createTodo).toHaveBeenCalledWith({
title: 'New Todo',
completed: false
})
})
it('should toggle todo completion', async () => {
mockTodosApi.fetchTodos.mockResolvedValue({
data: mockTodos,
message: 'Success',
success: true,
timestamp: new Date()
})
mockTodosApi.updateTodo.mockResolvedValue({
data: { ...mockTodos[0], completed: true },
message: 'Updated',
success: true,
timestamp: new Date()
})
const wrapper = mountWithPinia(TodoList)
await waitForAsyncComponent(wrapper)
const checkbox = wrapper.find('[data-testid="todo-checkbox-1"]')
await checkbox.setChecked(true)
expect(mockTodosApi.updateTodo).toHaveBeenCalledWith('1', {
completed: true
})
})
it('should display loading state', async () => {
// Mock a slow API response
mockTodosApi.fetchTodos.mockImplementation(() =>
new Promise(resolve =>
setTimeout(() => resolve({
data: mockTodos,
message: 'Success',
success: true,
timestamp: new Date()
}), 100)
)
)
const wrapper = mountWithPinia(TodoList)
// Should show loading initially
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(true)
await waitForAsyncComponent(wrapper)
// Should hide loading after data loads
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(false)
})
it('should display error state', async () => {
mockTodosApi.fetchTodos.mockRejectedValue(new Error('Network error'))
const wrapper = mountWithPinia(TodoList)
await waitForAsyncComponent(wrapper)
expect(wrapper.find('[data-testid="error"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="error"]').text()).toContain('Network error')
})
})
Testing Store Persistence
typescript
// src/stores/__tests__/persistence.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { useUserStore } from '@stores/user'
import { setupStoreTest } from '@/test/store-utils'
describe('Store Persistence', () => {
setupStoreTest()
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
})
afterEach(() => {
localStorage.clear()
sessionStorage.clear()
})
it('should persist user data to localStorage', () => {
const store = useUserStore()
const userData = {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
}
store.$patch({ user: userData, isAuthenticated: true })
const stored = localStorage.getItem('user-store')
expect(stored).toBeTruthy()
const parsed = JSON.parse(stored!)
expect(parsed.user).toEqual(userData)
expect(parsed.isAuthenticated).toBe(true)
})
it('should restore user data from localStorage', () => {
const userData = {
id: '1',
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
}
localStorage.setItem('user-store', JSON.stringify({
user: userData,
isAuthenticated: true
}))
const store = useUserStore()
expect(store.user).toEqual(userData)
expect(store.isAuthenticated).toBe(true)
})
it('should handle corrupted localStorage data', () => {
localStorage.setItem('user-store', 'invalid-json')
const store = useUserStore()
// Should fallback to default state
expect(store.user).toBeNull()
expect(store.isAuthenticated).toBe(false)
})
it('should clear persistence on logout', async () => {
const store = useUserStore()
store.$patch({
user: { id: '1', email: 'test@example.com' },
isAuthenticated: true
})
expect(localStorage.getItem('user-store')).toBeTruthy()
await store.logout()
expect(localStorage.getItem('user-store')).toBeNull()
})
})
End-to-End Testing
Playwright E2E Tests
typescript
// tests/e2e/todos.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Todo Application E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('should create and manage todos', async ({ page }) => {
// Create a new todo
await page.fill('[data-testid="todo-input"]', 'Buy groceries')
await page.click('[data-testid="add-todo-btn"]')
// Verify todo appears in list
await expect(page.locator('[data-testid="todo-item"]')).toContainText('Buy groceries')
// Mark todo as completed
await page.check('[data-testid="todo-checkbox"]')
// Verify todo is marked as completed
await expect(page.locator('[data-testid="todo-item"]')).toHaveClass(/completed/)
// Delete todo
await page.click('[data-testid="delete-todo-btn"]')
// Verify todo is removed
await expect(page.locator('[data-testid="todo-item"]')).toHaveCount(0)
})
test('should persist todos across page reloads', async ({ page }) => {
// Create a todo
await page.fill('[data-testid="todo-input"]', 'Persistent todo')
await page.click('[data-testid="add-todo-btn"]')
// Reload page
await page.reload()
// Verify todo persists
await expect(page.locator('[data-testid="todo-item"]')).toContainText('Persistent todo')
})
test('should filter todos correctly', async ({ page }) => {
// Create multiple todos
await page.fill('[data-testid="todo-input"]', 'Todo 1')
await page.click('[data-testid="add-todo-btn"]')
await page.fill('[data-testid="todo-input"]', 'Todo 2')
await page.click('[data-testid="add-todo-btn"]')
// Mark first todo as completed
await page.check('[data-testid="todo-checkbox"]:first-child')
// Filter by completed
await page.click('[data-testid="filter-completed"]')
// Should only show completed todo
await expect(page.locator('[data-testid="todo-item"]')).toHaveCount(1)
await expect(page.locator('[data-testid="todo-item"]')).toContainText('Todo 1')
// Filter by active
await page.click('[data-testid="filter-active"]')
// Should only show active todo
await expect(page.locator('[data-testid="todo-item"]')).toHaveCount(1)
await expect(page.locator('[data-testid="todo-item"]')).toContainText('Todo 2')
})
test('should handle network errors gracefully', async ({ page }) => {
// Intercept API calls and simulate network error
await page.route('**/api/todos', route => {
route.abort('failed')
})
await page.goto('/')
// Should display error message
await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
// Should show retry button
await expect(page.locator('[data-testid="retry-btn"]')).toBeVisible()
})
})
Performance Testing
Store Performance Tests
typescript
// src/stores/__tests__/performance.test.ts
import { describe, it, expect } from 'vitest'
import { useProductsStore } from '@stores/products'
import { setupStoreTest } from '@/test/store-utils'
describe('Store Performance', () => {
setupStoreTest()
it('should handle large datasets efficiently', () => {
const store = useProductsStore()
// Generate large dataset
const products = Array.from({ length: 10000 }, (_, i) => ({
id: `product-${i}`,
name: `Product ${i}`,
price: Math.random() * 100,
category: `Category ${i % 10}`,
inStock: Math.random() > 0.5
}))
const start = performance.now()
store.$patch({ products })
const patchTime = performance.now() - start
// Should patch large dataset quickly (< 100ms)
expect(patchTime).toBeLessThan(100)
const filterStart = performance.now()
// Test filtering performance
store.setFilters({ category: 'Category 1', inStock: true })
const filtered = store.filteredProducts
const filterTime = performance.now() - filterStart
// Should filter quickly (< 50ms)
expect(filterTime).toBeLessThan(50)
expect(filtered.length).toBeGreaterThan(0)
})
it('should debounce search operations', async () => {
const store = useProductsStore()
let searchCallCount = 0
const originalSearch = store.searchProducts
store.searchProducts = (...args) => {
searchCallCount++
return originalSearch.apply(store, args)
}
// Rapid search calls
store.searchProducts('test')
store.searchProducts('test1')
store.searchProducts('test12')
store.searchProducts('test123')
// Wait for debounce
await new Promise(resolve => setTimeout(resolve, 350))
// Should only call search once due to debouncing
expect(searchCallCount).toBe(1)
})
})
Coverage and Reporting
Coverage Configuration
json
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:coverage:ui": "vitest --coverage --ui",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
Custom Test Reporter
typescript
// src/test/reporter.ts
import type { Reporter } from 'vitest'
export class StoreTestReporter implements Reporter {
onInit() {
console.log('🧪 Starting Pinia store tests...')
}
onFinished(files, errors) {
const storeTests = files.filter(file =>
file.name.includes('stores/__tests__')
)
const totalStoreTests = storeTests.reduce(
(acc, file) => acc + (file.result?.numTotalTests || 0),
0
)
const passedStoreTests = storeTests.reduce(
(acc, file) => acc + (file.result?.numPassedTests || 0),
0
)
console.log(`\n📊 Store Test Summary:`)
console.log(` Total store tests: ${totalStoreTests}`)
console.log(` Passed: ${passedStoreTests}`)
console.log(` Failed: ${totalStoreTests - passedStoreTests}`)
if (errors.length > 0) {
console.log(`\n❌ Errors: ${errors.length}`)
}
}
}
This comprehensive testing guide provides patterns and examples for thoroughly testing Pinia stores, from basic unit tests to complex integration scenarios and end-to-end testing strategies.