Testing Migration Guide
This guide helps you maintain and improve testing strategies during the migration from Vuex to Pinia, ensuring code quality and functional integrity throughout the migration process.
Overview
Testing migration is a critical part of the entire state management migration process, requiring consideration of:
- Maintaining existing Vuex tests
- Writing new Pinia tests
- Parallel testing during migration
- Adapting testing tools and frameworks
- Maintaining test coverage
Test Environment Setup
1. Basic Test Configuration
ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.ts']
}
})
ts
// tests/setup.ts
import { config } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { createStore } from 'vuex'
// Global test setup
beforeEach(() => {
// Create new Pinia instance for each test
setActivePinia(createPinia())
})
// Configure Vue Test Utils
config.global.plugins = [
createPinia(),
// Add Vuex store if needed
]
2. Test Utility Functions
ts
// tests/utils/test-helpers.ts
import { createPinia, setActivePinia, type Pinia } from 'pinia'
import { createStore, type Store } from 'vuex'
import { mount, type VueWrapper } from '@vue/test-utils'
import type { Component } from 'vue'
// Pinia test helper
export function createTestPinia(): Pinia {
const pinia = createPinia()
setActivePinia(pinia)
return pinia
}
// Vuex test helper
export function createTestVuexStore(modules: any = {}): Store<any> {
return createStore({
modules,
strict: false // Disable strict mode in test environment
})
}
// Component mounting helper
export function mountWithStores(
component: Component,
options: {
pinia?: Pinia
vuexStore?: Store<any>
props?: Record<string, any>
[key: string]: any
} = {}
): VueWrapper {
const { pinia = createTestPinia(), vuexStore, props, ...mountOptions } = options
const plugins = [pinia]
if (vuexStore) {
plugins.push(vuexStore)
}
return mount(component, {
props,
global: {
plugins,
...mountOptions.global
},
...mountOptions
})
}
// Async operation wait helper
export async function waitForStoreAction(
action: () => Promise<any>,
timeout: number = 1000
): Promise<void> {
const start = Date.now()
await action()
// Wait for all microtasks to complete
await new Promise(resolve => setTimeout(resolve, 0))
if (Date.now() - start > timeout) {
throw new Error(`Store action timeout after ${timeout}ms`)
}
}
Vuex Test Migration
1. Existing Vuex Test Pattern
ts
// tests/store/user.test.ts (Vuex version)
import { createStore } from 'vuex'
import userModule from '@/store/modules/user'
describe('User Store (Vuex)', () => {
let store: any
beforeEach(() => {
store = createStore({
modules: {
user: {
...userModule,
namespaced: true
}
}
})
})
describe('mutations', () => {
it('should set user', () => {
const user = { id: 1, name: 'John Doe' }
store.commit('user/SET_USER', user)
expect(store.state.user.user).toEqual(user)
expect(store.getters['user/isLoggedIn']).toBe(true)
})
it('should clear user on logout', () => {
store.commit('user/SET_USER', { id: 1, name: 'John Doe' })
store.commit('user/LOGOUT')
expect(store.state.user.user).toBeNull()
expect(store.getters['user/isLoggedIn']).toBe(false)
})
})
describe('actions', () => {
it('should login user', async () => {
const mockApi = vi.fn().mockResolvedValue({ id: 1, name: 'John Doe' })
// Mock API
vi.doMock('@/api/auth', () => ({ login: mockApi }))
await store.dispatch('user/login', { email: 'test@example.com', password: 'password' })
expect(mockApi).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password' })
expect(store.getters['user/isLoggedIn']).toBe(true)
})
})
})
2. Converting to Pinia Tests
ts
// tests/stores/user.test.ts (Pinia version)
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
import { createTestPinia } from '../utils/test-helpers'
// Mock API
vi.mock('@/api/auth', () => ({
login: vi.fn(),
logout: vi.fn()
}))
import { login as mockLogin } from '@/api/auth'
describe('User Store (Pinia)', () => {
beforeEach(() => {
createTestPinia()
vi.clearAllMocks()
})
describe('state management', () => {
it('should initialize with default state', () => {
const store = useUserStore()
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
it('should update user state', () => {
const store = useUserStore()
const user = { id: 1, name: 'John Doe', email: 'john@example.com' }
// Direct state modification (Pinia allows this)
store.$patch({ user })
expect(store.user).toEqual(user)
expect(store.isLoggedIn).toBe(true)
expect(store.fullName).toBe('John Doe')
})
})
describe('actions', () => {
it('should login user successfully', async () => {
const store = useUserStore()
const credentials = { email: 'test@example.com', password: 'password' }
const userData = { id: 1, name: 'John', lastName: 'Doe', email: 'test@example.com' }
;(mockLogin as any).mockResolvedValue(userData)
await store.login(credentials)
expect(mockLogin).toHaveBeenCalledWith(credentials)
expect(store.user).toEqual(userData)
expect(store.isLoggedIn).toBe(true)
})
it('should handle login error', async () => {
const store = useUserStore()
const error = new Error('Invalid credentials')
;(mockLogin as any).mockRejectedValue(error)
await expect(store.login({ email: 'test@example.com', password: 'wrong' }))
.rejects.toThrow('Invalid credentials')
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
it('should logout user', async () => {
const store = useUserStore()
// Set initial user
store.$patch({ user: { id: 1, name: 'John Doe' } })
expect(store.isLoggedIn).toBe(true)
await store.logout()
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
})
describe('getters', () => {
it('should compute full name correctly', () => {
const store = useUserStore()
store.$patch({
user: { id: 1, name: 'John', lastName: 'Doe' }
})
expect(store.fullName).toBe('John Doe')
})
it('should return empty string for missing name parts', () => {
const store = useUserStore()
store.$patch({
user: { id: 1, name: 'John' }
})
expect(store.fullName).toBe('John ')
})
})
})
Component Testing
1. Testing Components with Vuex
ts
// tests/components/UserProfile.test.ts (Vuex version)
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import UserProfile from '@/components/UserProfile.vue'
import userModule from '@/store/modules/user'
describe('UserProfile Component (Vuex)', () => {
let store: any
beforeEach(() => {
store = createStore({
modules: {
user: {
...userModule,
namespaced: true
}
}
})
})
it('should display user information', () => {
const user = { id: 1, name: 'John Doe', email: 'john@example.com' }
store.commit('user/SET_USER', user)
const wrapper = mount(UserProfile, {
global: {
plugins: [store]
}
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('should handle logout action', async () => {
const user = { id: 1, name: 'John Doe' }
store.commit('user/SET_USER', user)
const wrapper = mount(UserProfile, {
global: {
plugins: [store]
}
})
await wrapper.find('[data-testid="logout-button"]').trigger('click')
expect(store.state.user.user).toBeNull()
})
})
2. Testing Components with Pinia
ts
// tests/components/UserProfile.test.ts (Pinia version)
import { mount } from '@vue/test-utils'
import UserProfile from '@/components/UserProfile.vue'
import { useUserStore } from '@/stores/user'
import { createTestPinia, mountWithStores } from '../utils/test-helpers'
// Mock API
vi.mock('@/api/auth', () => ({
logout: vi.fn().mockResolvedValue(undefined)
}))
describe('UserProfile Component (Pinia)', () => {
beforeEach(() => {
createTestPinia()
})
it('should display user information', () => {
const wrapper = mountWithStores(UserProfile)
const store = useUserStore()
// Set user data
store.$patch({
user: { id: 1, name: 'John Doe', email: 'john@example.com' }
})
expect(wrapper.text()).toContain('John Doe')
expect(wrapper.text()).toContain('john@example.com')
})
it('should handle logout action', async () => {
const wrapper = mountWithStores(UserProfile)
const store = useUserStore()
// Set initial user
store.$patch({
user: { id: 1, name: 'John Doe' }
})
await wrapper.find('[data-testid="logout-button"]').trigger('click')
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
it('should show login form when not logged in', () => {
const wrapper = mountWithStores(UserProfile)
const store = useUserStore()
// Ensure user is not logged in
expect(store.isLoggedIn).toBe(false)
expect(wrapper.find('[data-testid="login-form"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="user-info"]').exists()).toBe(false)
})
})
Advanced Testing Patterns
1. Testing Store Composition
ts
// tests/stores/composition.test.ts
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { createTestPinia } from '../utils/test-helpers'
describe('Store Composition', () => {
beforeEach(() => {
createTestPinia()
})
it('should work with multiple stores', () => {
const userStore = useUserStore()
const cartStore = useCartStore()
// Set up user
userStore.$patch({
user: { id: 1, name: 'John Doe' }
})
// Add items to cart
cartStore.addItem({ id: 1, name: 'Product 1', price: 10 })
cartStore.addItem({ id: 2, name: 'Product 2', price: 20 })
expect(userStore.isLoggedIn).toBe(true)
expect(cartStore.itemCount).toBe(2)
expect(cartStore.total).toBe(30)
})
it('should handle cross-store dependencies', async () => {
const userStore = useUserStore()
const cartStore = useCartStore()
// Mock checkout API
vi.mock('@/api/checkout', () => ({
checkout: vi.fn().mockResolvedValue({ orderId: '123' })
}))
// Set up stores
userStore.$patch({ user: { id: 1, name: 'John Doe' } })
cartStore.addItem({ id: 1, name: 'Product 1', price: 10 })
// Perform checkout (which depends on both stores)
const result = await cartStore.checkout()
expect(result.orderId).toBe('123')
expect(cartStore.items).toHaveLength(0) // Cart should be cleared
})
})
2. Testing with Mocked Stores
ts
// tests/components/ProductList.test.ts
import { mount } from '@vue/test-utils'
import ProductList from '@/components/ProductList.vue'
import { createTestPinia } from '../utils/test-helpers'
import { useProductStore } from '@/stores/product'
// Create mock store
const mockProductStore = {
products: [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 }
],
loading: false,
error: null,
fetchProducts: vi.fn(),
addProduct: vi.fn()
}
describe('ProductList Component', () => {
it('should display products', () => {
const pinia = createTestPinia({
createSpy: vi.fn,
stubActions: false
})
const wrapper = mount(ProductList, {
global: {
plugins: [pinia]
}
})
const store = useProductStore()
// Mock store state
store.$patch(mockProductStore)
expect(wrapper.findAll('[data-testid="product-item"]')).toHaveLength(2)
expect(wrapper.text()).toContain('Product 1')
expect(wrapper.text()).toContain('Product 2')
})
it('should handle loading state', () => {
const pinia = createTestPinia()
const wrapper = mount(ProductList, {
global: { plugins: [pinia] }
})
const store = useProductStore()
store.$patch({ loading: true })
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(true)
})
})
3. Testing SSR Scenarios
ts
// tests/ssr/state-serialization.test.ts
import { createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
import { createTestPinia } from '../utils/test-helpers'
describe('SSR State Serialization', () => {
it('should serialize and deserialize state correctly', () => {
// Server-side: create and populate store
const serverPinia = createPinia()
const serverStore = useUserStore(serverPinia)
serverStore.$patch({
user: { id: 1, name: 'John Doe', email: 'john@example.com' }
})
// Serialize state (as would happen on server)
const serializedState = JSON.stringify(serverPinia.state.value)
// Client-side: create new store and hydrate
const clientPinia = createPinia()
const clientStore = useUserStore(clientPinia)
// Hydrate state (as would happen on client)
clientPinia.state.value = JSON.parse(serializedState)
// Verify state was transferred correctly
expect(clientStore.user).toEqual({
id: 1,
name: 'John Doe',
email: 'john@example.com'
})
expect(clientStore.isLoggedIn).toBe(true)
})
it('should handle partial state hydration', () => {
const pinia = createTestPinia()
const store = useUserStore()
// Simulate partial state from server
const partialState = {
user: {
user: { id: 1, name: 'John Doe' }
// Missing other properties
}
}
pinia.state.value = partialState
expect(store.user).toEqual({ id: 1, name: 'John Doe' })
expect(store.isLoggedIn).toBe(true)
})
})
Migration Testing Strategies
1. Parallel Testing
ts
// tests/migration/parallel-stores.test.ts
import { createStore } from 'vuex'
import { createTestPinia } from '../utils/test-helpers'
import { useUserStore as usePiniaUserStore } from '@/stores/user'
import vuexUserModule from '@/store/modules/user'
describe('Parallel Store Testing', () => {
it('should maintain same behavior between Vuex and Pinia', async () => {
// Test Vuex implementation
const vuexStore = createStore({
modules: {
user: { ...vuexUserModule, namespaced: true }
}
})
const userData = { id: 1, name: 'John Doe', email: 'john@example.com' }
// Vuex operations
vuexStore.commit('user/SET_USER', userData)
const vuexIsLoggedIn = vuexStore.getters['user/isLoggedIn']
// Test Pinia implementation
createTestPinia()
const piniaStore = usePiniaUserStore()
// Pinia operations
piniaStore.$patch({ user: userData })
const piniaIsLoggedIn = piniaStore.isLoggedIn
// Both should behave the same
expect(vuexIsLoggedIn).toBe(piniaIsLoggedIn)
expect(vuexStore.state.user.user).toEqual(piniaStore.user)
})
})
2. Regression Testing
ts
// tests/migration/regression.test.ts
import { createTestPinia } from '../utils/test-helpers'
import { useUserStore } from '@/stores/user'
describe('Migration Regression Tests', () => {
beforeEach(() => {
createTestPinia()
})
// Test cases that ensure migrated functionality works as expected
const testCases = [
{
name: 'should handle user login flow',
async test() {
const store = useUserStore()
// Mock successful login
vi.mocked(mockLogin).mockResolvedValue({
id: 1,
name: 'John',
lastName: 'Doe',
email: 'john@example.com'
})
await store.login({ email: 'john@example.com', password: 'password' })
expect(store.isLoggedIn).toBe(true)
expect(store.fullName).toBe('John Doe')
}
},
{
name: 'should handle user logout flow',
async test() {
const store = useUserStore()
// Set initial state
store.$patch({ user: { id: 1, name: 'John Doe' } })
expect(store.isLoggedIn).toBe(true)
await store.logout()
expect(store.isLoggedIn).toBe(false)
expect(store.user).toBeNull()
}
}
]
testCases.forEach(({ name, test }) => {
it(name, test)
})
})
Performance Testing
1. Store Performance
ts
// tests/performance/store-performance.test.ts
import { createTestPinia } from '../utils/test-helpers'
import { useUserStore } from '@/stores/user'
describe('Store Performance', () => {
beforeEach(() => {
createTestPinia()
})
it('should handle large state updates efficiently', () => {
const store = useUserStore()
const largeDataSet = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`
}))
const start = performance.now()
store.$patch({ users: largeDataSet })
const end = performance.now()
const duration = end - start
// Should complete within reasonable time (adjust threshold as needed)
expect(duration).toBeLessThan(100) // 100ms
expect(store.users).toHaveLength(10000)
})
it('should handle frequent updates without memory leaks', () => {
const store = useUserStore()
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0
// Perform many updates
for (let i = 0; i < 1000; i++) {
store.$patch({ user: { id: i, name: `User ${i}` } })
}
// Force garbage collection if available
if (global.gc) {
global.gc()
}
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
const memoryIncrease = finalMemory - initialMemory
// Memory increase should be reasonable (adjust threshold as needed)
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024) // 10MB
})
})
Best Practices
1. Test Organization
tests/
├── utils/
│ ├── test-helpers.ts
│ ├── mocks.ts
│ └── fixtures.ts
├── stores/
│ ├── user.test.ts
│ ├── cart.test.ts
│ └── product.test.ts
├── components/
│ ├── UserProfile.test.ts
│ ├── ProductList.test.ts
│ └── ShoppingCart.test.ts
├── migration/
│ ├── parallel-stores.test.ts
│ ├── regression.test.ts
│ └── compatibility.test.ts
├── integration/
│ ├── user-flow.test.ts
│ └── checkout-flow.test.ts
└── performance/
├── store-performance.test.ts
└── component-performance.test.ts
2. Testing Guidelines
- Isolate tests: Each test should be independent and not rely on others
- Mock external dependencies: Use mocks for APIs, localStorage, etc.
- Test behavior, not implementation: Focus on what the store does, not how
- Use descriptive test names: Make it clear what each test is verifying
- Test edge cases: Include error conditions and boundary cases
- Maintain test coverage: Ensure all critical paths are tested
3. Common Pitfalls
- Shared state between tests: Always reset store state between tests
- Async timing issues: Properly wait for async operations to complete
- Mock leakage: Clear mocks between tests to avoid interference
- Over-mocking: Don't mock everything; test real behavior when possible
- Ignoring error cases: Test both success and failure scenarios
Continuous Integration
1. CI Configuration
yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
2. Test Scripts
json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:watch": "vitest --watch",
"test:migration": "vitest tests/migration",
"test:performance": "vitest tests/performance"
}
}
Conclusion
Testing during migration from Vuex to Pinia requires careful planning and execution. Key takeaways:
- Maintain existing tests while gradually migrating to Pinia patterns
- Use parallel testing to ensure behavioral consistency
- Focus on integration tests to catch migration issues
- Automate regression testing to prevent breaking changes
- Monitor performance throughout the migration process
By following these practices, you can ensure a smooth migration while maintaining code quality and confidence in your application's behavior.