This episode covers the Zustand testing strategy with Vitest: unit tests for state and actions, testing the persist middleware with a mocked storage, and component testing using React Testing Library. You also learn the reset-store-between-tests pattern so every test runs from a clean state.

A store is the part of application logic most worth testing: pure, deterministic, and DOM-free. Episode 18 covers the Zustand testing strategy with Vitest — unit tests for state and actions, testing the persist middleware with mocked storage, and component testing with React Testing Library to make sure components respond to state correctly.
You also learn the reset-store-between-tests pattern so every test runs from a clean state.
Install Vitest and React Testing Library:
npm i -D vitest @testing-library/react @testing-library/jest-domThen write a test for a simple store. Because a store is just a function, the test can poke getState and call actions directly:
import { describe, expect, it } from 'vitest'
import { useCounter } from './counter-store'
describe('counter store', () => {
it('increment menambah count dari 0 menjadi 1', () => {
expect(useCounter.getState().count).toBe(0)
useCounter.getState().increment()
expect(useCounter.getState().count).toBe(1)
})
})useCounter.getState().increment() runs the action without any rendering. This test verifies the store logic in isolation from the UI.
For async actions that fetch data, mock fetch and make sure the state changes after the promise resolves:
it('memuat user setelah fetch selesai', async () => {
vi.stubGlobal('fetch', vi.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ name: 'Arman' }) }),
))
await useUserStore.getState().loadUser(1)
expect(useUserStore.getState().user.name).toBe('Arman')
expect(useUserStore.getState().loading).toBe(false)
vi.unstubAllGlobals()
})await useUserStore.getState().loadUser(1) waits for the async action to finish, then checks the result. This is the same pattern for loading status and error handling.
Persist reads storage at rehydration. To test it, mock the storage with a simple object and control the rehydration timing:
import { createJSONStorage } from 'zustand/middleware'
it('me-rehydrate state dari storage', () => {
const storage = createJSONStorage(() => ({
getItem: () => JSON.stringify({ state: { theme: 'dark' } }),
setItem: () => {},
removeItem: () => {},
}))
const store = create(persist((set) => ({ theme: 'light' }), {
name: 'theme-store',
storage,
}))
expect(store.getState().theme).toBe('dark')
})A getItem returning the stored state makes persist immediately rehydrate the theme value to dark when the store is created. For async storage, use waitFor or polling until the state is populated.
For components that use hooks, render with the testing library and simulate interactions:
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
it('menampilkan tombol Tambah', async () => {
render(<Counter />)
await userEvent.click(screen.getByRole('button', { name: 'Tambah' }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})userEvent.click simulates a real click and triggers the store action, then the render is checked via the text that appears. Component tests complement unit tests: the first verifies logic, the second verifies the UI is wired up correctly.
Store state is global and persists between tests. Reset it in beforeEach so each test starts from the initial state:
import { beforeEach } from 'vitest'
beforeEach(() => {
useCounter.setState({ count: 0 })
useUserStore.setState({ user: null, loading: false })
})setState({ count: 0 }) returns the store to its initial state without rendering components. This prevents tests from affecting each other and makes every test deterministic.
Episode 18 completes the safety net: unit tests for store logic with direct getState, async action tests with mocked fetch, persist tests with mocked storage, component tests with React Testing Library, and state resets in beforeEach so every test is clean.
Key takeaways:
In the next episode we will discuss optimization and debugging — profiling re-renders with React Profiler and Redux DevTools, then common troubleshooting like infinite renders, state that doesn't update, hydration mismatches, and stale closures.