# Test stores and components

Test actions through the store API, then mount the connected React component and test what the user sees. Reset the stores after every test so one test's updates do not become another test's starting state. Use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) for components and [Mock Service Worker (MSW)](https://mswjs.io/) for network requests, without replacing your application's actions or `fetch`.

The examples below render a counter starting at `1`. Clicking **One Up** changes it to `2`; clicking **Load greeting** displays the response text from your greeting endpoint. The tests supply their own greeting through MSW.

## 1. Add the testing tools

Start in a TypeScript React project with [Zustand](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand) installed. Choose one runner. Both setups use JSDOM to mount React components in a DOM environment.

### Vitest

```bash
npm install -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom msw
```

### Jest

```bash
npm install -D jest jest-environment-jsdom ts-jest ts-node @types/jest @testing-library/react @testing-library/user-event @testing-library/jest-dom msw
```

Jest and Vitest have different module-loading conventions. Use the configuration for your runner rather than mixing Jest's CommonJS mock loading with Vitest's ES module loading. For projects with additional transforms, follow the [Jest setup documentation](https://jestjs.io/docs/getting-started) or [Vitest setup documentation](https://vitest.dev/guide).

## 2. Create the store and connected component

Keep [`create`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#create) and the [`StateCreator`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#statecreator)-typed initializer in application code so the tests exercise the real counter and network actions; see [Subscribe outside React](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/subscribe-outside-react) for the store API used to inspect and reset state.

```ts title="src/store.ts"
import { create, type StateCreator } from 'zustand'

export type CounterState = {
  count: number
  greeting: string
  inc: () => void
  loadGreeting: () => Promise<void>
}

const counterStoreCreator: StateCreator<CounterState> = (set) => ({
  count: 1,
  greeting: '',
  inc: () => set((state) => ({ count: state.count + 1 })),
  loadGreeting: async () => {
    const response = await fetch('https://api.example.com/greeting')
    if (!response.ok) {
      throw new Error(`Greeting request failed: ${response.status}`)
    }
    const greeting = await response.text()
    set({ greeting })
  },
})

export const useCounterStore = create<CounterState>()(counterStoreCreator)
```

Replace `https://api.example.com/greeting` with your endpoint in the application and the test handler. This example expects a text response.

Select the values and actions the component needs. No provider is required for this bound store.

```tsx title="src/Counter.tsx"
import * as React from 'react'
import { useCounterStore } from './store'

export function Counter() {
  const count = useCounterStore((state) => state.count)
  const inc = useCounterStore((state) => state.inc)
  const greeting = useCounterStore((state) => state.greeting)
  const loadGreeting = useCounterStore((state) => state.loadGreeting)

  return (
    <section>
      <h2>Counter Store</h2>
      <p aria-label="Count">{count}</p>
      <button onClick={inc}>One Up</button>
      <button onClick={() => void loadGreeting()}>Load greeting</button>
      <p role="status">{greeting}</p>
    </section>
  )
}
```

The tests below mount this component with `render(<Counter />)`. They check both the store's state and the rendered count after an update.

You can also connect a component through [`useStore`](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/zustand#usestore), passing the store API and a selector. This alternative counter uses the same store and renders the same count and increment button.

```tsx title="src/CounterWithUseStore.tsx"
import * as React from 'react'
import { useStore } from 'zustand'
import { useCounterStore } from './store'

export function CounterWithUseStore() {
  const count = useStore(useCounterStore, (state) => state.count)
  const inc = useStore(useCounterStore, (state) => state.inc)

  return (
    <section>
      <h2>Counter Store</h2>
      <p aria-label="Count">{count}</p>
      <button onClick={inc}>One Up</button>
    </section>
  )
}
```

![The counter displays its initial count of 1 and the One Up button.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/a88adffb176ac321e2695a6e83bc6d8b.png)

## 3. Reset state and intercept requests

Put the shared test support in one file. `getInitialState()` returns the state created by the initializer; `getState()` returns the current state. Restore the complete initial state with `setState(initialState, true)`, following the authors' reset pattern. Keep this reset in test teardown, not in application actions.

```ts title="tests/support.ts"
import { act, cleanup } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { useCounterStore } from '../src/store'

const initialState = useCounterStore.getInitialState()
const storeResetFns = new Set<() => void>([
  () => useCounterStore.setState(initialState, true),
])

export const server = setupServer(
  http.get('https://api.example.com/greeting', () =>
    HttpResponse.text('Hello from the test'),
  ),
)

export function resetAfterTest() {
  cleanup()
  act(() => {
    storeResetFns.forEach((reset) => reset())
  })
  server.resetHandlers()
}
```

Add a reset function for each additional shared store your tests use. This example registers the real store explicitly: it does not mock the `zustand` module. MSW intercepts the request while the real `loadGreeting()` action still runs.

### Vitest setup

```ts title="tests/setup-vitest.ts"
import '@testing-library/jest-dom/vitest'
import { afterAll, afterEach, beforeAll } from 'vitest'
import { resetAfterTest, server } from './support'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(resetAfterTest)
afterAll(() => server.close())
```

```ts title="vitest.config.ts"
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./tests/setup-vitest.ts'],
    include: ['tests/**/*.vitest.test.tsx'],
  },
})
```

### Jest setup

```ts title="tests/setup-jest.ts"
import '@testing-library/jest-dom/jest-globals'
import { afterAll, afterEach, beforeAll } from '@jest/globals'
import { resetAfterTest, server } from './support'

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(resetAfterTest)
afterAll(() => server.close())
```

```ts title="jest.config.ts"
const config = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['./tests/setup-jest.ts'],
  testMatch: ['**/tests/**/*.jest.test.tsx'],
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      { tsconfig: { strict: true, jsx: 'react-jsx', module: 'CommonJS' } },
    ],
  },
}

export default config
```

Use a Node runtime and test environment compatible with your installed MSW version. Follow [MSW's Node integration instructions](https://mswjs.io/docs/integrations/node) for any required environment support.

## 4. Assert actions, rendered updates, and asynchronous results

Use your runner's test file below. The first test calls the real action inside `act()` on a mounted component. The second uses the button, checking that the counter starts at `1` again. The third waits for the network action's result in the DOM rather than sleeping for a fixed duration.

### Vitest tests

```tsx title="tests/counter.vitest.test.tsx"
import * as React from 'react'
import { expect, test } from 'vitest'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from '../src/Counter'
import { useCounterStore } from '../src/store'

test('the action updates the store and its connected component', () => {
  render(<Counter />)
  expect(useCounterStore.getState().count).toBe(1)
  act(() => useCounterStore.getState().inc())
  expect(useCounterStore.getState().count).toBe(2)
  expect(screen.getByLabelText('Count')).toHaveTextContent('2')
})

test('a button updates the counter from its initial state', async () => {
  const user = userEvent.setup()
  render(<Counter />)
  expect(screen.getByLabelText('Count')).toHaveTextContent('1')
  await user.click(screen.getByRole('button', { name: 'One Up' }))
  expect(screen.getByLabelText('Count')).toHaveTextContent('2')
  expect(useCounterStore.getState().count).toBe(2)
})

test('the real network action displays the intercepted response', async () => {
  const user = userEvent.setup()
  render(<Counter />)
  await user.click(screen.getByRole('button', { name: 'Load greeting' }))
  expect(await screen.findByText('Hello from the test')).toBeInTheDocument()
  expect(useCounterStore.getState().greeting).toBe('Hello from the test')
})
```

```bash
npx vitest run
```

### Jest tests

```tsx title="tests/counter.jest.test.tsx"
import * as React from 'react'
import { expect, test } from '@jest/globals'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from '../src/Counter'
import { useCounterStore } from '../src/store'

test('the action updates the store and its connected component', () => {
  render(<Counter />)
  expect(useCounterStore.getState().count).toBe(1)
  act(() => useCounterStore.getState().inc())
  expect(useCounterStore.getState().count).toBe(2)
  expect(screen.getByLabelText('Count')).toHaveTextContent('2')
})

test('a button updates the counter from its initial state', async () => {
  const user = userEvent.setup()
  render(<Counter />)
  expect(screen.getByLabelText('Count')).toHaveTextContent('1')
  await user.click(screen.getByRole('button', { name: 'One Up' }))
  expect(screen.getByLabelText('Count')).toHaveTextContent('2')
  expect(useCounterStore.getState().count).toBe(2)
})

test('the real network action displays the intercepted response', async () => {
  const user = userEvent.setup()
  render(<Counter />)
  await user.click(screen.getByRole('button', { name: 'Load greeting' }))
  expect(await screen.findByText('Hello from the test')).toBeInTheDocument()
  expect(useCounterStore.getState().greeting).toBe('Hello from the test')
})
```

```bash
npx jest
```

`Hello from the test` is the fixture supplied by the handler, not a promise about what your real service returns. In your application, the status paragraph displays your endpoint's response text.

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `setState`'s `replace` argument | `boolean` | For object updates, omitted means merge | Pass `true` in teardown to restore the complete initial state rather than shallow-merge it with the current state. |

## Pitfalls

- A module-level store survives component unmounts. Reset every shared store used by the suite; DOM cleanup alone does not reset its state.
- Wrap direct store updates affecting mounted components in `act()`, including teardown resets. Await user interactions and asynchronous DOM assertions.
- If you adopt automatic Zustand mocks, put Vitest's manual-mock directory under its configured `root`. The explicit-reset setup above needs no manual-mock directory.
- For replacement-state typing and preservation of actions, see [Type stores and middleware](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/type-stores-and-middleware). For selector stability, see [Selectors and subscriptions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/selectors-and-subscriptions).

## Live demos

Explore the authors' [Jest testing demo](https://stackblitz.com/edit/jest-zustand) or [Vitest testing demo](https://stackblitz.com/edit/vitest-zustand) for their automatic store-reset mock and counter component tests.

## Related

- [Reset store state](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/reset-store-state) — reset one store or multiple stores.
- [Initialize scoped stores](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/initialize-scoped-stores) — supply a scoped store through React context.
- [State and actions](https://bench-zustand-61.atloria.app/p/bench-zustand-61-pSJCWrWW5Y/developer/state-and-actions) — immutable updates and colocated actions.
