Appearance
Pinia 测试
为 Pinia Store 编写测试是确保应用状态管理正确性和可靠性的重要环节。本指南将详细介绍如何为 Pinia Store 编写单元测试、集成测试和端到端测试。
测试环境设置
安装测试依赖
bash
# Jest
npm install -D jest @types/jest
# 或者使用 Vitest (推荐)
npm install -D vitest @vitest/ui jsdom
# Vue 测试工具
npm install -D @vue/test-utils
测试配置
javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\js$': 'babel-jest',
},
collectCoverageFrom: [
'src/stores/**/*.js',
'!src/stores/index.js',
],
}
或者使用 Vitest:
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.js'],
},
})
基础测试设置
创建测试辅助函数
javascript
// tests/helpers/pinia.js
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
export const createTestPinia = (options = {}) => {
const testingPinia = createTestingPinia({
createSpy: options.createSpy || vi.fn, // 使用 Vitest 的 mock 函数
stubActions: options.stubActions !== false, // 默认模拟 Actions
})
setActivePinia(testingPinia)
return testingPinia
}
测试示例 Store
javascript
// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'counter',
}),
getters: {
doubleCount: (state) => state.count * 2,
isEven: (state) => state.count % 2 === 0,
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(amount) {
this.count += amount
},
async asyncIncrement() {
await new Promise(resolve => setTimeout(resolve, 100))
this.increment()
return this.count
},
},
})
单元测试
测试 State
javascript
// tests/unit/counter.store.spec.js
import { describe, it, expect, beforeEach } from 'vitest'
import { createTestPinia } from '../helpers/pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store - State', () => {
let store
beforeEach(() => {
createTestPinia()
store = useCounterStore()
})
it('初始化时 count 为 0', () => {
expect(store.count).toBe(0)
})
it('初始化时 name 为 counter', () => {
expect(store.name).toBe('counter')
})
it('初始状态可以被覆盖用于测试', () => {
const pinia = createTestPinia({
initialState: {
counter: { count: 10, name: 'test-counter' },
},
})
const testStore = useCounterStore(pinia)
expect(testStore.count).toBe(10)
expect(testStore.name).toBe('test-counter')
})
})
测试 Getters
javascript
// 继续在 tests/unit/counter.store.spec.js 中
describe('Counter Store - Getters', () => {
let store
beforeEach(() => {
createTestPinia()
store = useCounterStore()
})
it('doubleCount 返回 count 的两倍', () => {
store.count = 5
expect(store.doubleCount).toBe(10)
})
it('isEven 正确判断奇偶性', () => {
store.count = 4
expect(store.isEven).toBe(true)
store.count = 5
expect(store.isEven).toBe(false)
})
it('getters 是响应式的', () => {
store.count = 2
expect(store.doubleCount).toBe(4)
store.count = 3
expect(store.doubleCount).toBe(6) // 应该自动更新
})
})
测试 Actions
javascript
// 继续在 tests/unit/counter.store.spec.js 中
describe('Counter Store - Actions', () => {
let store
beforeEach(() => {
createTestPinia()
store = useCounterStore()
})
it('increment 增加 count 1', () => {
store.count = 5
store.increment()
expect(store.count).toBe(6)
})
it('decrement 减少 count 1', () => {
store.count = 5
store.decrement()
expect(store.count).toBe(4)
})
it('incrementBy 按指定数量增加', () => {
store.count = 5
store.incrementBy(3)
expect(store.count).toBe(8)
})
it('asyncIncrement 异步增加 count', async () => {
store.count = 5
const result = await store.asyncIncrement()
expect(store.count).toBe(6)
expect(result).toBe(6)
})
})
测试复杂 Store
带有副作用的 Store
javascript
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false,
error: null,
}),
getters: {
isLoggedIn: (state) => !!state.user,
userName: (state) => state.user?.name || 'Guest',
},
actions: {
async fetchUser(id) {
this.loading = true
this.error = null
try {
// 模拟 API 调用
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error('Failed to fetch user')
}
this.user = await response.json()
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
setUser(userData) {
this.user = userData
},
clearUser() {
this.user = null
this.error = null
},
},
})
测试异步操作
javascript
// tests/unit/user.store.spec.js
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { createTestPinia } from '../helpers/pinia'
import { useUserStore } from '@/stores/user'
describe('User Store - Async Operations', () => {
let store
beforeEach(() => {
// 创建模拟的 fetch 函数
global.fetch = vi.fn()
store = useUserStore(createTestPinia())
})
afterEach(() => {
vi.clearAllMocks()
})
it('fetchUser 成功获取用户数据', async () => {
const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' }
global.fetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
})
await store.fetchUser(1)
expect(store.loading).toBe(false)
expect(store.user).toEqual(mockUser)
expect(store.error).toBeNull()
expect(global.fetch).toHaveBeenCalledWith('/api/users/1')
})
it('fetchUser 处理 API 错误', async () => {
global.fetch.mockResolvedValue({
ok: false,
status: 404,
})
await expect(store.fetchUser(999)).rejects.toThrow('Failed to fetch user')
expect(store.loading).toBe(false)
expect(store.user).toBeNull()
expect(store.error).toBe('Failed to fetch user')
})
it('setUser 正确设置用户数据', () => {
const userData = { id: 1, name: 'Jane Doe' }
store.setUser(userData)
expect(store.user).toEqual(userData)
expect(store.isLoggedIn).toBe(true)
expect(store.userName).toBe('Jane Doe')
})
it('clearUser 清除用户数据', () => {
store.setUser({ id: 1, name: 'John' })
store.clearUser()
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
expect(store.userName).toBe('Guest')
})
})
组件集成测试
测试使用 Store 的组件
vue
<!-- src/components/Counter.vue -->
<template>
<div>
<p>Count: {{ store.count }}</p>
<p>Double: {{ store.doubleCount }}</p>
<button @click="store.increment">Increment</button>
<button @click="store.decrement">Decrement</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
javascript
// tests/components/Counter.spec.js
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestPinia } from '../helpers/pinia'
import Counter from '@/components/Counter.vue'
import { useCounterStore } from '@/stores/counter'
describe('Counter Component', () => {
let wrapper
let store
beforeEach(() => {
const pinia = createTestPinia()
wrapper = mount(Counter, {
global: {
plugins: [pinia],
},
})
store = useCounterStore()
})
it('显示正确的初始计数值', () => {
expect(wrapper.text()).toContain('Count: 0')
expect(wrapper.text()).toContain('Double: 0')
})
it('点击 Increment 按钮增加计数', async () => {
const incrementBtn = wrapper.find('button')
await incrementBtn.trigger('click')
expect(wrapper.text()).toContain('Count: 1')
expect(wrapper.text()).toContain('Double: 2')
})
it('点击 Decrement 按钮减少计数', async () => {
// 先增加到 1
store.increment()
await wrapper.vm.$nextTick()
const decrementBtn = wrapper.findAll('button')[1]
await decrementBtn.trigger('click')
expect(wrapper.text()).toContain('Count: 0')
})
})
Mock 和 Stub 技术
Mock Actions
javascript
// tests/unit/mock-actions.spec.js
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createTestPinia } from '../helpers/pinia'
import { useUserStore } from '@/stores/user'
describe('Mock Actions', () => {
let store
beforeEach(() => {
// 自动模拟所有 Actions
const pinia = createTestPinia({ stubActions: true })
store = useUserStore(pinia)
})
it('actions 被正确模拟', () => {
store.fetchUser(1) // 这不会执行实际的 API 调用
expect(store.fetchUser).toHaveBeenCalled() // 检查是否被调用
})
it('可以手动模拟特定 action', async () => {
// 手动模拟 action
store.fetchUser = vi.fn().mockResolvedValue({ id: 1, name: 'Mock User' })
const result = await store.fetchUser(1)
expect(result).toEqual({ id: 1, name: 'Mock User' })
expect(store.fetchUser).toHaveBeenCalledWith(1)
})
})
依赖注入测试
javascript
// src/stores/api.js
import { defineStore } from 'pinia'
export const useApiStore = defineStore('api', (deps = {}) => {
const { apiClient = defaultApiClient } = deps
const data = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
data.value = await apiClient.get('/data')
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData,
}
})
// 测试依赖注入
// tests/unit/api.store.spec.js
describe('API Store with Dependency Injection', () => {
it('使用提供的依赖项', async () => {
const mockApiClient = {
get: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
}
const pinia = createTestPinia()
const store = useApiStore(pinia, { apiClient: mockApiClient })
await store.fetchData()
expect(mockApiClient.get).toHaveBeenCalledWith('/data')
expect(store.data).toEqual([{ id: 1, name: 'Test' }])
})
})
测试最佳实践
1. 测试状态转换
javascript
// 测试状态机类型的状态转换
import { useAuthStore } from '@/stores/auth'
describe('Auth Store - State Transitions', () => {
let store
beforeEach(() => {
store = useAuthStore(createTestPinia())
})
it('正确处理登录状态转换', async () => {
expect(store.status).toBe('unauthenticated')
// 模拟登录过程
await store.login({ email: 'test@example.com', password: 'password' })
expect(store.status).toBe('authenticated')
expect(store.user).not.toBeNull()
})
it('正确处理登出状态转换', () => {
store.setUser({ id: 1, name: 'Test User' })
store.logout()
expect(store.status).toBe('unauthenticated')
expect(store.user).toBeNull()
})
})
2. 测试错误边界
javascript
describe('Error Handling', () => {
let store
beforeEach(() => {
store = useUserStore(createTestPinia())
})
it('在 action 失败时设置错误状态', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
})
await expect(store.fetchUser(1)).rejects.toThrow()
expect(store.error).not.toBeNull()
expect(store.loading).toBe(false)
})
})
3. 测试持久化
javascript
// 如果使用了持久化插件
import { createTestingPinia } from '@pinia/testing'
describe('Persistence Tests', () => {
it('状态正确持久化', () => {
const pinia = createTestingPinia({
initialState: {
counter: { count: 42 },
},
})
const store = useCounterStore(pinia)
expect(store.count).toBe(42)
})
})
测试运行和报告
运行测试
bash
# 运行所有测试
npm test
# 运行特定测试文件
npm test tests/unit/counter.store.spec.js
# 监听模式
npm test -- --watch
# 生成覆盖率报告
npm test -- --coverage
通过以上测试策略和示例,您可以为 Pinia Store 编写全面的测试,确保状态管理的可靠性和正确性。