Pinia 测试策略 #
概述 #
测试是确保代码质量的重要环节。Pinia 提供了专门的测试工具,使得测试 Store 变得简单直观。
测试环境配置 #
Vitest 配置 #
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
}
})
安装测试依赖 #
bash
npm install -D vitest @vue/test-utils @pinia/testing jsdom
单元测试 #
测试 State #
ts
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'counter'
})
})
ts
// tests/stores/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
// 每个测试前创建新的 Pinia 实例
setActivePinia(createPinia())
})
it('should have initial state', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
expect(store.name).toBe('counter')
})
it('should update state', () => {
const store = useCounterStore()
store.count = 5
expect(store.count).toBe(5)
})
it('should use $patch to update state', () => {
const store = useCounterStore()
store.$patch({ count: 10, name: 'myCounter' })
expect(store.count).toBe(10)
expect(store.name).toBe('myCounter')
})
it('should reset state', () => {
const store = useCounterStore()
store.count = 100
store.$reset()
expect(store.count).toBe(0)
})
})
测试 Getters #
ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2,
isPositive: (state) => state.count > 0
}
})
ts
// tests/stores/counter.test.ts
describe('Getters', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should compute doubleCount', () => {
const store = useCounterStore()
expect(store.doubleCount).toBe(0)
store.count = 5
expect(store.doubleCount).toBe(10)
})
it('should compute isPositive', () => {
const store = useCounterStore()
expect(store.isPositive).toBe(false)
store.count = 1
expect(store.isPositive).toBe(true)
store.count = -1
expect(store.isPositive).toBe(false)
})
})
测试 Actions #
ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
incrementBy(value: number) {
this.count += value
},
async fetchCount() {
const response = await fetch('/api/count')
const data = await response.json()
this.count = data.count
}
}
})
ts
// tests/stores/counter.test.ts
describe('Actions', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should increment count', () => {
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1)
store.increment()
expect(store.count).toBe(2)
})
it('should increment by value', () => {
const store = useCounterStore()
store.incrementBy(5)
expect(store.count).toBe(5)
store.incrementBy(10)
expect(store.count).toBe(15)
})
it('should fetch count', async () => {
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ count: 42 })
} as Response)
)
const store = useCounterStore()
await store.fetchCount()
expect(store.count).toBe(42)
expect(fetch).toHaveBeenCalledWith('/api/count')
})
})
使用 @pinia/testing #
createTestingPinia #
ts
import { createTestingPinia } from '@pinia/testing'
describe('With Testing Pinia', () => {
it('should mock actions', () => {
const pinia = createTestingPinia({
createSpy: vi.fn,
initialState: {
counter: { count: 10 }
}
})
setActivePinia(pinia)
const store = useCounterStore()
// 初始状态
expect(store.count).toBe(10)
// Actions 被自动 mock
store.increment()
expect(store.increment).toHaveBeenCalledTimes(1)
expect(store.count).toBe(10) // 状态未改变(因为 action 被 mock)
})
})
配置选项 #
ts
const pinia = createTestingPinia({
// 是否 stub actions
stubActions: false, // false 时执行真实 actions
// 初始状态
initialState: {
counter: { count: 5 },
user: { name: 'John' }
},
// 创建 spy 的函数
createSpy: vi.fn,
// 插件
plugins: []
})
测试真实 Actions #
ts
it('should execute real actions', () => {
const pinia = createTestingPinia({
stubActions: false
})
setActivePinia(pinia)
const store = useCounterStore()
store.increment()
expect(store.count).toBe(1) // 真实执行
})
组件测试 #
测试组件与 Store 的交互 #
vue
<!-- components/Counter.vue -->
<template>
<div>
<p data-testid="count">{{ counter.count }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
ts
// tests/components/Counter.test.ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import Counter from '@/components/Counter.vue'
import { useCounterStore } from '@/stores/counter'
describe('Counter Component', () => {
it('should display count', () => {
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
initialState: {
counter: { count: 5 }
}
})
]
}
})
expect(wrapper.find('[data-testid="count"]').text()).toBe('5')
})
it('should call increment on button click', async () => {
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia()]
}
})
const store = useCounterStore()
await wrapper.find('button').trigger('click')
expect(store.increment).toHaveBeenCalled()
})
})
测试 Store 订阅 #
ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
ts
// tests/stores/counter.test.ts
describe('Store Subscriptions', () => {
it('should call $subscribe callback', () => {
setActivePinia(createPinia())
const store = useCounterStore()
const callback = vi.fn()
store.$subscribe(callback)
store.count = 5
expect(callback).toHaveBeenCalled()
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({ type: 'direct' }),
expect.objectContaining({ count: 5 })
)
})
it('should call $onAction callback', () => {
setActivePinia(createPinia())
const store = useCounterStore()
const callback = vi.fn()
store.$onAction(callback)
store.increment()
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
name: 'increment',
args: [],
store: expect.anything()
})
)
})
})
测试异步 Actions #
使用 vi.mock #
ts
// api/user.ts
export const api = {
async getUser() {
const response = await fetch('/api/user')
return response.json()
}
}
ts
// stores/user.ts
import { defineStore } from 'pinia'
import { api } from '@/api/user'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false
}),
actions: {
async fetchUser() {
this.loading = true
try {
this.user = await api.getUser()
} finally {
this.loading = false
}
}
}
})
ts
// tests/stores/user.test.ts
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
vi.mock('@/api/user', () => ({
api: {
getUser: vi.fn()
}
}))
describe('User Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should fetch user', async () => {
const mockUser = { id: 1, name: 'John' }
const { api } = await import('@/api/user')
vi.mocked(api.getUser).mockResolvedValue(mockUser)
const store = useUserStore()
await store.fetchUser()
expect(store.user).toEqual(mockUser)
expect(store.loading).toBe(false)
})
it('should handle fetch error', async () => {
const { api } = await import('@/api/user')
vi.mocked(api.getUser).mockRejectedValue(new Error('Network error'))
const store = useUserStore()
await expect(store.fetchUser()).rejects.toThrow('Network error')
expect(store.loading).toBe(false)
})
})
测试 Store 组合 #
ts
// stores/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
getters: {
summary(): string {
const userStore = useUserStore()
return `${userStore.name}'s cart`
}
},
actions: {
checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('Not logged in')
}
// ...
}
}
})
ts
// tests/stores/cart.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
describe('Cart Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should generate summary with user name', () => {
const userStore = useUserStore()
userStore.name = 'John'
const cartStore = useCartStore()
expect(cartStore.summary).toBe("John's cart")
})
it('should throw error when not logged in', () => {
const userStore = useUserStore()
userStore.isLoggedIn = false
const cartStore = useCartStore()
expect(() => cartStore.checkout()).toThrow('Not logged in')
})
})
最佳实践 #
1. 每个测试使用新的 Pinia 实例 #
ts
beforeEach(() => {
setActivePinia(createPinia())
})
2. 使用 TypeScript 类型 #
ts
import type { Store } from 'pinia'
it('should have correct types', () => {
const store = useCounterStore() as Store<'counter', CounterState, CounterGetters, CounterActions>
// TypeScript 会提供类型检查
})
3. Mock 外部依赖 #
ts
// 使用 vi.mock mock API 调用
vi.mock('@/api', () => ({
api: {
getUsers: vi.fn()
}
}))
4. 测试边界情况 #
ts
describe('Edge cases', () => {
it('should handle empty state', () => {
const store = useUserStore()
expect(store.user).toBeNull()
})
it('should handle negative values', () => {
const store = useCounterStore()
store.count = -5
expect(store.count).toBe(-5)
})
})
下一步 #
现在你已经掌握了 Pinia 的测试策略,接下来让我们学习最佳实践。
- 项目结构 - 代码组织规范
最后更新:2026-03-28