跳至内容

测试存储

存储库设计上将在许多地方使用,并且可能使测试比应有的难度更大。幸运的是,情况并非必须如此。在测试存储库时,我们需要注意三件事

  • pinia 实例:存储库无法在没有它的情况下工作
  • actions:大多数情况下,它们包含存储库中最复杂的逻辑。如果它们默认情况下被模拟,那不是很好吗?
  • 插件:如果您依赖于插件,则也必须为测试安装它们

根据您正在测试的内容或方式,我们需要以不同的方式处理这三件事。

单元测试存储库

要单元测试存储库,最重要的部分是创建一个 pinia 实例

js
// stores/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    // creates a fresh pinia and makes it active
    // so it's automatically picked up by any useStore() call
    // without having to pass it to it: `useStore(pinia)`
    setActivePinia(createPinia())
  })

  it('increments', () => {
    const counter = useCounterStore()
    expect(counter.n).toBe(0)
    counter.increment()
    expect(counter.n).toBe(1)
  })

  it('increments by amount', () => {
    const counter = useCounterStore()
    counter.increment(10)
    expect(counter.n).toBe(10)
  })
})

如果您有任何存储库插件,有一件重要的事情需要知道:插件在 pinia 安装在 App 中之前不会使用。这可以通过创建一个空的 App 或一个假的 App 来解决

js
import { setActivePinia, createPinia } from 'pinia'
import { createApp } from 'vue'
import { somePlugin } from '../src/stores/plugin'

// same code as above...

// you don't need to create one app per test
const app = createApp({})
beforeEach(() => {
  const pinia = createPinia().use(somePlugin)
  app.use(pinia)
  setActivePinia(pinia)
})

单元测试组件

这可以通过 createTestingPinia() 来实现,它返回一个旨在帮助单元测试组件的 pinia 实例。

首先安装 @pinia/testing

shell
npm i -D @pinia/testing

并确保在挂载组件时在测试中创建一个测试 pinia

js
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
// import any store you want to interact with in tests
import { useSomeStore } from '@/stores/myStore'

const wrapper = mount(Counter, {
  global: {
    plugins: [createTestingPinia()],
  },
})

const store = useSomeStore() // uses the testing pinia!

// state can be directly manipulated
store.name = 'my new name'
// can also be done through patch
store.$patch({ name: 'new name' })
expect(store.name).toBe('new name')

// actions are stubbed by default, meaning they don't execute their code by default.
// See below to customize this behavior.
store.someAction()

expect(store.someAction).toHaveBeenCalledTimes(1)
expect(store.someAction).toHaveBeenLastCalledWith()

请注意,如果您使用的是 Vue 2,@vue/test-utils 需要稍微不同的配置

初始状态

您可以在创建测试 pinia 时通过传递一个 initialState 对象来设置所有存储库的初始状态。此对象将被测试 pinia 用于在创建存储库时修补它们。假设您想初始化此存储库的状态

ts
import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
  state: () => ({ n: 0 }),
  // ...
})

由于存储库名为“counter”,因此您需要在 initialState 中添加一个匹配的对象

ts
// somewhere in your test
const wrapper = mount(Counter, {
  global: {
    plugins: [
      createTestingPinia({
        initialState: {
          counter: { n: 20 }, // start the counter at 20 instead of 0
        },
      }),
    ],
  },
})

const store = useSomeStore() // uses the testing pinia!
store.n // 20

自定义操作的行为

createTestingPinia 会屏蔽所有存储库操作,除非另有说明。这使您可以分别测试组件和存储库。

如果您想恢复此行为并在测试期间正常执行操作,请在调用 createTestingPinia 时指定 stubActions: false

js
const wrapper = mount(Counter, {
  global: {
    plugins: [createTestingPinia({ stubActions: false })],
  },
})

const store = useSomeStore()

// Now this call WILL execute the implementation defined by the store
store.someAction()

// ...but it's still wrapped with a spy, so you can inspect calls
expect(store.someAction).toHaveBeenCalledTimes(1)

模拟操作的返回值

操作会自动被间谍,但从类型上讲,它们仍然是常规操作。为了获得正确的类型,我们必须实现一个自定义类型包装器,它将 Mock 类型应用于每个操作。此类型取决于您正在使用的测试框架。以下是用 Vitest 的示例

ts
import type { Mock } from 'vitest'
import type { Store, StoreDefinition } from 'pinia'

function mockedStore<TStoreDef extends () => unknown>(
  useStore: TStoreDef
): TStoreDef extends StoreDefinition<
  infer Id,
  infer State,
  infer Getters,
  infer Actions
>
  ? Store<
      Id,
      State,
      Record<string, never>,
      {
        [K in keyof Actions]: Actions[K] extends (
          ...args: infer Args
        ) => infer ReturnT
          ? // 👇 depends on your testing framework
            Mock<Args, ReturnT>
          : Actions[K]
      }
    > & {
      [K in keyof Getters]: Getters[K] extends ComputedRef<infer T> ? T : never
    }
  : ReturnType<TStoreDef> {
  return useStore() as any
}

这可以在测试中使用以获得正确类型的存储库

ts
import { mockedStore } from './mockedStore'
import { useSomeStore } from '@/stores/myStore'

const store = mockedStore(useSomeStore)
// typed!
store.someAction.mockResolvedValue('some value')

如果您有兴趣学习更多类似的技巧,您应该查看Mastering Pinia上的测试课程。

指定 createSpy 函数

当使用 Jest 或带有 globals: true 的 vitest 时,createTestingPinia 会自动使用基于现有测试框架(jest.fnvitest.fn)的间谍函数来屏蔽操作。如果您没有使用 globals: true 或使用其他框架,则需要提供一个createSpy 选项

ts
// NOTE: not needed with `globals: true`
import { vi } from 'vitest'

createTestingPinia({
  createSpy: vi.fn,
})
ts
import sinon from 'sinon'

createTestingPinia({
  createSpy: sinon.spy,
})

您可以在测试包的测试中找到更多示例。

模拟获取器

默认情况下,任何获取器都将像常规用法一样计算,但您可以通过将获取器设置为任何您想要的值来手动强制一个值

ts
import { defineStore } from 'pinia'
import { createTestingPinia } from '@pinia/testing'

const useCounterStore = defineStore('counter', {
  state: () => ({ n: 1 }),
  getters: {
    double: (state) => state.n * 2,
  },
})

const pinia = createTestingPinia()
const counter = useCounterStore(pinia)

counter.double = 3 // 🪄 getters are writable only in tests

// set to undefined to reset the default behavior
// @ts-expect-error: usually it's a number
counter.double = undefined
counter.double // 2 (=1 x 2)

Pinia 插件

如果您有任何 pinia 插件,请确保在调用 createTestingPinia() 时传递它们,以便正确应用它们。不要像使用常规 pinia 一样使用 testingPinia.use(MyPlugin) 添加它们

js
import { createTestingPinia } from '@pinia/testing'
import { somePlugin } from '../src/stores/plugin'

// inside some test
const wrapper = mount(Counter, {
  global: {
    plugins: [
      createTestingPinia({
        stubActions: false,
        plugins: [somePlugin],
      }),
    ],
  },
})

E2E 测试

对于 Pinia,您不需要为 E2E 测试更改任何内容,这就是这些测试的全部意义!您可能可以测试 HTTP 请求,但这超出了本指南的范围😄。

单元测试组件(Vue 2)

当使用Vue Test Utils 1 时,在 localVue 上安装 Pinia

js
import { PiniaVuePlugin } from 'pinia'
import { createLocalVue, mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'

const localVue = createLocalVue()
localVue.use(PiniaVuePlugin)

const wrapper = mount(Counter, {
  localVue,
  pinia: createTestingPinia(),
})

const store = useSomeStore() // uses the testing pinia!