How to Test Pinia Stores with Vitest

In this tutorial, we will learn how to test Pinia stores with Vitest. We will create an example store and implement unit tests for it step-by-step.

But hold on a second, what should we test in a Pinia store?

We have to assert that:

  • Getters return the correct values.
  • Actions are consuming direct dependencies correctly and updating the state accordingly.

You can find the source code on GitHub. Let’s get started!

Example Store

Let’s create an example store that we can use to demonstrate implementing unit tests. The store will manage a list of customers. It will have an action to fetch customers and two getters that will return computed values.

First, create a type file to store a Customer type:

export type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

Next, create a service file that a store’s action will use:

import { Customer } from '../stores/customers/index.store'

export const fetchCustomers = (): Promise<Customer[]> => {
  return new Promise((resolve) => {
    resolve([
      {
        id: '1',
        fullName: 'John Doe',
        isActive: true,
        totalSpending: 3000,
      },
      {
        id: '2',
        fullName: 'John Cena',
        isActive: false,
        totalSpending: 5000,
      },
    ])
  })
}

The service file exports a function to fetch customers’ data. Instead of making an HTTP request, return a promise with hardcoded data to keep it simple.

Finally, create the store:

import { defineStore } from 'pinia'
import { fetchCustomers } from '../../services/customers.service'

export type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestPending: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({
    customers: [],
    isRequestPending: false,
  }),
  getters: {
    activeCustomersCount({ customers }) {
      return customers.filter(({ isActive }) => isActive).length
    },
    totalCustomerSpending({ customers }) {
      return customers.reduce((totalSPending, customer) => {
        return totalSPending + customer.totalSpending
      }, 0)
    },
  },
  actions: {
    async fetchCustomers(): Promise<void> {
      this.isRequestPending = true
      this.customers = await fetchCustomers()
      this.isRequestPending = false
    },
  },
})

For simplicity’s sake, we kept everything in one file. However, actions and getters and the state should be in separate files in a real-world application.

We have an action that fetches the customers and saves them in the state. Furthermore, we have two getters that return the active customer count and the total customer spending.

Testing Getters

We have two getters we want to test: activeCustomersCount and totalCustomerSpending.

First, set up the spec file:

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCustomers } from './index.store'
import { createPinia } from 'pinia'
import { Customer } from '../../types/Customer'

describe('Customers Store', () => {
  describe('getters', () => {
    describe('activeCustomersCount', () => {
      it.todo('returns the number of active customers')
    }

    describe('totalCustomersSpending', () => {
      it.todo('returns the total spending of all customers')
    }
  })
})

Start by implementing the unit test for activeCustomersCount. Create a new store by calling useCustomers. Pass a Pinia instance as an argument by calling createPinia.

We need a Pinia instance for the store to work since it will be attached to it:

describe('activeCustomersCount', () => {
  it('returns the number of active customers', () => {
    const store = useCustomers(createPinia())
  })
})

We need a fresh pinia instance for each unit test. Using a single instance will use the same state from an older test, which might lead to false positives. Hence, we call createPinia in each test; we will refactor this later!

Next, hydrate the store with some data, one customer with an active account and another one with an inactive account:

describe('activeCustomersCount', () => {
  it('returns the number of active customers', () => {
    const store = useCustomers(createPinia())

    store.customers = [
      {
        id: '1',
        fullName: 'John Doe',
        isActive: true,
        totalSpending: 3000,
      },
      {
        id: '2',
        fullName: 'John Cena',
        isActive: false,
        totalSpending: 5000,
      },
    ]
  })
})

Finally, assert that the number of active customers is 1:

describe('activeCustomersCount', () => {
  it('returns the number of active customers', () => {
    const store = useCustomers(createPinia())

    //...code

    expect(store.activeCustomersCount).toBe(1)
  })
})
Passing Vitest Pinia Unit Test

Moving on to the next getter, follow the same pattern:

describe('totalCustomerSpending', () => {
  it('returns the total spending of all customers', () => {
    const store = useCustomers(createPinia())

    store.customers = [
      {
        id: '1',
        fullName: 'John Doe',
        isActive: true,
        totalSpending: 3000,
      },
      {
        id: '2',
        fullName: 'John Cena',
        isActive: false,
        totalSpending: 5000,
      },
    ]

    expect(store.totalCustomersSpending).toBe(8000)
  })
})
Passing Pinia Vitest Unit Tests

Since we are seeding the state with the same data, refactor that into a function.

const getCustomers = (): Customer[] => [
  {
    id: '1',
    fullName: 'John Doe',
    isActive: true,
    totalSpending: 3000,
  },
  {
    id: '2',
    fullName: 'John Cena',
    isActive: false,
    totalSpending: 5000,
  },
]

describe('Customers Store', () => {
  describe('getters', () => {
    describe('activeCustomersCount', () => {
      it('returns the number of active customers', () => {
        const store = useCustomers(createPinia())

        store.customers = getCustomers()

        expect(store.activeCustomersCount).toBe(1)
      })
    })

    describe('totalCustomersSpending', () => {
      it('returns the total spending of all customers', () => {
        const store = useCustomers(createPinia())

        store.customers = getCustomers()

        expect(store.totalCustomersSpending).toBe(8000)
      })
    })
  })
})

Moreover, instead of repeating the creation of a Pinia instance in every test, we can use setActivePinia in beforeEach. Then we will have a fresh Pinia instance for each test, and we can remove the call to createPinia in every store usage:

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCustomers } from './index.store'
import { createPinia, setActivePinia } from 'pinia'
import { Customer } from '../../types/Customer'

// ...code

describe('Customers Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  describe('getters', () => {
    describe('activeCustomersCount', () => {
      it('returns the number of active customers', () => {
        const store = useCustomers()

        // ...code
      })
    })

    describe('totalCustomersSpending', () => {
      it('returns the total spending of all customers', () => {
        const store = useCustomers()

        // ...code
      })
    })
  })
})

We have covered the basics of testing a store’s getters. There are more advanced examples where a getter might return a function. We will cover that in a later tutorial.

Let’s move on and implement unit tests for the action.

Testing Actions

Our action fetchCustomers uses the service method fetchCustomers to fetch customers. We want to assert that the fetchCustomers service method is called when calling the action. We don’t want, however, to make a real HTTP Request or call another service.

Hence, we mock the service and fake out the implementation of the method used in the action.

To mock a service, use vi.mock.

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCustomers } from './index.store'
import { createPinia } from 'pinia'
import { fetchCustomers } from '../../services/customers.service'
import { Customer } from '../../types/Customer'

vi.mock('../../services/customers.service')

This will turn every exported function into a Vitest MockedFunction. We can then use mock functions to change the behavior of this service method.

Let’s pretend we are doing an HTTP request to fetch the customers. We want to mock this function to return a hardcoded list of customers.

describe('actions', () => {
  describe('fetchCustomers', () => {
    it('calls fetchCustomers service method to fetches customers', async () => {
      const store = useCustomers(createPinia())
      const customers = getCustomers()

      vi.mocked(fetchCustomers).mockResolvedValue(customers)
    })
  })
})

Next, call the action from the store and assert that the hardcoded data returned by the service method is the same one set in the state:

describe('actions', () => {
  describe('fetchCustomers', () => {
    it('calls fetchCustomers service method to fetches customers', async () => {
      const store = useCustomers(createPinia())
      const customers = getCustomers()

      vi.mocked(fetchCustomers).mockResolvedValue(customers)

      await store.fetchCustomers()

      expect(fetchCustomers).toHaveBeenCalled()
      expect(store.customers).toStrictEqual(customers)
    })
  })
})

Finally, we should reset the mocked service method after changing its behavior before each test.

beforeEach(() => {
  setActivePinia(createPinia())
  vi.mocked(fetchCustomers).mockReset()
})

When running the tests, they should pass!

Passing test suite

Conclusion

In this tutorial, we learned how to test actions and getters. More advanced examples will be coming soon; stay tuned!

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19