Improve testing docs (#1875)

* Update testing docs (wip)

* WIP

* WIP

* Update testing docs

* Minor updates
This commit is contained in:
Danilo Britto 2023-06-22 17:58:13 -05:00 committed by GitHub
parent 7276b531bc
commit 9333eb629c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,105 +1,282 @@
---
title: Testing
description: How to test your new store
description: Writing Tests
nav: 9
---
## Resetting state between tests in **react-dom**
## Setting Up a Test Environment
When running tests, the stores are not automatically reset before each test run.
### Test Runners
Thus, there can be cases where the state of one test can affect another. To make sure all tests run
with a pristine store state, you can mock `zustand` during testing and use the following code to
create your store:
Usually, your test runner needs to be configured to run JavaScript/TypeScript syntax. If you're
going to be testing UI components, you will likely need to configure the test runner to use JSDOM
to provide a mock DOM environment.
```js
import { create as actualCreate } from 'zustand'
// const { create: actualCreate } = jest.requireActual('zustand') // if using jest
import { act } from 'react-dom/test-utils'
See these resources for test runner configuration instructions:
// a variable to hold reset functions for all stores declared in the app
const storeResetFns = new Set()
- **Jest**
- [Jest: Getting Started](https://jestjs.io/docs/getting-started)
- [Jest: Configuration - Test Environment](https://jestjs.io/docs/configuration#testenvironment-string)
- **Vitest**
- [Vitest: Getting Started](https://vitest.dev/guide)
- [Vitest: Configuration - Test Environment](https://vitest.dev/config/#environment)
// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (createState) => {
const store = actualCreate(createState)
const initialState = store.getState()
storeResetFns.add(() => store.setState(initialState, true))
return store
}
### UI and Network Testing Tools
// Reset all stores after each test run
beforeEach(() => {
act(() => storeResetFns.forEach((resetFn) => resetFn()))
})
```
**We recommend using [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro)
to test out React components that connect to Zustand**. RTL is a simple and complete React DOM
testing utility that encourages good testing practices. It uses ReactDOM's `render` function and
`act` from `react-dom/tests-utils`. Futhermore, [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro)
is the alternative to RTL to test out React Native components. The [Testing Library](https://testing-library.com/)
family of tools also includes adapters for many other popular frameworks.
The way you mock a dependency depends on your test runner/library.
We also recommend using [Mock Service Worker (MSW)](https://mswjs.io/) to mock network requests, as
this means your application logic does not need to be changed or mocked when writing tests.
In [jest](https://jestjs.io/), you can create a `__mocks__/zustand.js` and place the code in that
file. If your app is using `zustand/vanilla` instead of `zustand`, then you'll have to place the
above code in `__mocks__/zustand/vanilla.js`.
- **React Testing Library (DOM)**
- [DOM Testing Library: Setup](https://testing-library.com/docs/dom-testing-library/setup)
- [React Testing Library: Setup](https://testing-library.com/docs/react-testing-library/setup)
- [Testing Library Jest-DOM Matchers](https://testing-library.com/docs/ecosystem-jest-dom)
- **Native Testing Library (React Native)**
- [Native Testing Library: Setup](https://testing-library.com/docs/react-native-testing-library/setup)
- **User Event Testing Library (DOM)**
- [User Event Testing Library: Setup](https://testing-library.com/docs/user-event/setup)
- **TypeScript for Jest**
- [TypeScript for Jest: Setup](https://kulshekhar.github.io/ts-jest/docs/getting-started/installation)
- **TypeScript for Node**
- [TypeScript for Node: Setup](https://typestrong.org/ts-node/docs/installation)
- **Mock Service Worker**
- [MSW: Installation](https://mswjs.io/docs/getting-started/install)
- [MSW: Setting up mock requests](https://mswjs.io/docs/getting-started/mocks/rest-api)
- [MSW: Mock server configuration for Node](https://mswjs.io/docs/getting-started/integrate/node)
### TypeScript usage
## Setting Up Zustand for testing
If you are using zustand, as documented in [TypeScript Guide](./typescript.md), use the following
code:
Since Jest and Vitest have slight differences, like Vitest using **ES modules** and Jest using
**CommonJS modules**, you need to keep that in mind if you are using Vitest instead of Jest.
### Jest
In the next steps we are going to setup our Jest environment in order to mock Zustand
```ts
import { create as actualCreate, StateCreator } from 'zustand'
// if using Jest:
// import { StateCreator } from 'zustand';
// const { create: actualCreate } = jest.requireActual<typeof import('zustand')>('zustand');
import { act } from 'react-dom/test-utils'
// src/__mocks__/zustand.ts
import * as zustand from 'zustand'
import { act } from '@testing-library/react'
const { create: actualCreate } = jest.requireActual<typeof zustand>('zustand')
// a variable to hold reset functions for all stores declared in the app
const storeResetFns = new Set<() => void>()
export const storeResetFns = new Set<() => void>()
// when creating a store, we get its initial state, create a reset function and add it in the set
const create =
() =>
<S>(createState: StateCreator<S>) => {
const store = actualCreate(createState)
export const create = (<T extends unknown>() => {
console.log('zustand create mock')
return (stateCreator: zustand.StateCreator<T>) => {
const store = actualCreate(stateCreator)
const initialState = store.getState()
storeResetFns.add(() => store.setState(initialState, true))
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
}) as typeof zustand.create
// Reset all stores after each test run
beforeEach(() => {
act(() => storeResetFns.forEach((resetFn) => resetFn()))
// reset all stores after each test run
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn()
})
})
})
```
## Resetting state between tests in **react-native** and **jest**
```ts
// src/setup-jest.ts
import '@testing-library/jest-dom'
```
You should use the following code in the `__mocks__/zustand.js` file (the `__mocks__` directory
should be adjacent to node_modules, placed in the same folder as node_modules, unless you
configured roots to point to a folder other than the project root [jest docs: mocking node modules](https://jestjs.io/docs/manual-mocks#mocking-node-modules)):
```ts
// jest.config.ts
import type { JestConfigWithTsJest } from 'ts-jest'
```js
import { act } from '@testing-library/react-native'
const { create: actualCreate } = jest.requireActual('zustand')
// a variable to hold reset functions for all stores declared in the app
const storeResetFns = new Set()
// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (createState) => {
const store = actualCreate(createState)
const initialState = store.getState()
storeResetFns.add(() => store.setState(initialState, true))
return store
const config: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./src/setup-jest.ts'],
}
// Reset all stores after each test run
beforeEach(() => {
act(() => storeResetFns.forEach((resetFn) => resetFn()))
export default config
```
> **Note**: to use typescript we need to install two packages `ts-jest` and `ts-node`
### Vitest
In the next steps we are going to setup our Vitest environment in order to mock Zustand
```ts
// src/__mocks__/zustand.ts
import * as zustand from 'zustand'
import { act } from '@testing-library/react'
const { create: actualCreate } = await vi.importActual<typeof zustand>(
'zustand'
)
// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>()
// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T extends unknown>() => {
console.log('zustand create mock')
return (stateCreator: zustand.StateCreator<T>) => {
const store = actualCreate(stateCreator)
const initialState = store.getState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
}) as typeof zustand.create
// reset all stores after each test run
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn()
})
})
})
```
If the `jest.config.js` has `automock: false`, then you need to do the following in `jest.setup.js`:
> **Note:** without [globals configuration](https://vitest.dev/config/#globals) enabled, you need
> to add `import { afterEach, vi } from 'vitest'` at the top
```js
jest.mock('zustand')
```ts
// src/setup-vitest.ts
import '@testing-library/jest-dom'
vi.mock('zustand') // to make it works like Jest (auto-mocking)
```
> **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, you need
> to add `import { vi } from 'vitest'` at the top
```ts
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./setup-vitest.ts'],
},
})
)
```
### Testing components
In the next examples we are going to use `useCounterStore`
> **Note**: all of these examples are written using `TypeScript`
```ts
// stores/user-counter-store.ts
import { create } from 'zustand'
export type CounterStore = {
count: number
inc: () => void
}
export const useCounterStore = create<CounterStore>()((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
```
```tsx
// components/counter/counter.tsx
import { useCounterStore } from '../../stores/user-counter-store'
export function Counter() {
const { count, inc } = useCounterStore()
return (
<div>
<span>{count}</span>
<button onClick={inc}>one up</button>
</div>
)
}
```
```ts
// components/counter/index.ts
export * from './counter'
```
```tsx
// components/counter/counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './counter'
describe('Counter', () => {
test('should render successfully', async () => {
renderCounter()
expect(await screen.findByText(/^1$/)).toBeInTheDocument()
expect(
await screen.findByRole('button', { name: /one up/i })
).toBeInTheDocument()
})
test('should increase count by clicking a button', async () => {
const user = userEvent.setup()
renderCounter()
expect(await screen.findByText(/^1$/)).toBeInTheDocument()
await user.click(await screen.findByRole('button', { name: /one up/ }))
expect(await screen.findByText(/^2$/)).toBeInTheDocument()
})
})
const renderCounter = () => {
return render(<Counter />)
}
```
> **Note**: without [globals configuration](https://vitest.dev/config/#globals) enabled, you need
> to add `import { describe, test, expect } from 'vitest'` at the top of each test file
**CodeSandbox Demos:**
- Jest Demo: https://codesandbox.io/p/sandbox/friendly-breeze-276c28
- Vitest Demo: https://codesandbox.io/p/sandbox/zustand-vitest-demo-ph5gnj
## References
- **React Testing Library**: [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro)
is a very lightweight solution for testing React components. It provides utility functions on top
of `react-dom` and `react-dom/test-utils`, in a way that encourages better testing practices. Its
primary guiding principle is: "The more your tests resemble the way your software is used, the
more confidence they can give you."
- **Native Testing Library**: [Native Testing Library (RNTL)](https://testing-library.com/docs/react-native-testing-library/intro)
is a very lightweight solution for testing React Native components, similarly to RTL, but its
functions are built on top of `react-test-renderer`.
- **Testing Implementation Details**: Blog post by Kent C. Dodds on why he recommends to avoid
[testing implementation details](https://kentcdodds.com/blog/testing-implementation-details).