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