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)
})
})
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)
})
})
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!
Conclusion
In this tutorial, we learned how to test actions and getters. More advanced examples will be coming soon; stay tuned!