Learning Zustand - Testing Stores & Middleware
Episode 18 of 23

Learning Zustand - Testing Stores & Middleware

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.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

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.

Unit Testing Stores with Vitest

Install Vitest and React Testing Library:

Install testing tooling
npm i -D vitest @testing-library/react @testing-library/jest-dom

Then write a test for a simple store. Because a store is just a function, the test can poke getState and call actions directly:

JSUnit test for a counter store
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.

Async Actions

For async actions that fetch data, mock fetch and make sure the state changes after the promise resolves:

JSTesting an async action
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.

Testing the Persist Middleware

Persist reads storage at rehydration. To test it, mock the storage with a simple object and control the rehydration timing:

JSTesting persist with a mocked storage
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.

Component Testing with React Testing Library

For components that use hooks, render with the testing library and simulate interactions:

JSTesting the Counter component
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.

Resetting Stores Between Tests

Store state is global and persists between tests. Reset it in beforeEach so each test starts from the initial state:

JSResetting stores between tests
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.

Closing

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:

  • Stores can be tested without a DOM via getState and direct action calls.
  • Async actions are tested with mocked fetch and awaiting the action.
  • The persist middleware is tested with a mocked storage that returns state.
  • Component tests make sure the UI is wired to the store.
  • Reset stores in beforeEach to keep every test deterministic.
  • The combination of unit and component tests gives full confidence.

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.

Learning Zustand - Testing Stores & Middleware | Learning Zustand