Обычно программа запуска тестов должна быть настроена на выполнение синтаксиса JavaScript/TypeScript. Если вы собираетесь тестировать компоненты пользовательского интерфейса, вам, вероятно, потребуется настроить программу для тестирования на использование JSDOM для создания имитации DOM-окружения.
Инструкции по настройке тестового прогона см. на этих ресурсах:
Инструменты для тестирования пользовательского интерфейса и сети¶
Мы рекомендуем использовать React Testing Library (RTL) для тестирования компонентов React, подключаемых к Zustand. RTL - это простая и полная утилита для тестирования React DOM, которая поощряет хорошие практики тестирования. Она использует функцию render ReactDOM и act из react-dom/tests-utils. Кроме того, Native Testing Library (RNTL) - это альтернатива RTL для тестирования компонентов React Native. Семейство инструментов Testing Library также включает адаптеры для многих других популярных фреймворков.
Мы также рекомендуем использовать Mock Service Worker (MSW) для имитации сетевых запросов, так как это означает, что логику вашего приложения не нужно менять или имитировать при написании тестов.
Примечание: Поскольку Jest и Vitest имеют небольшие различия, например, Vitest использует ES модули, а Jest использует CommonJS модули, вам нужно иметь это в виду, если вы используете Vitest вместо Jest.
Макет, представленный ниже, позволит соответствующему тестовому прогону сбрасывать хранилища zustand после каждого теста.
Этот общий код был добавлен, чтобы избежать дублирования кода в нашем демо, поскольку мы используем одного и того же создателя хранилища для обеих реализаций, с API Context и без него - createStore и create, соответственно.
// __mocks__/zustand.tsimport*aszustandfrom'zustand';import{act}from'@testing-library/react';const{create:actualCreate,createStore:actualCreateStore,}=jest.requireActual<typeofzustand>('zustand');// a variable to hold reset functions for all stores declared in the appexportconststoreResetFns=newSet<()=>void>();constcreateUncurried=<T>(stateCreator:zustand.StateCreator<T>)=>{conststore=actualCreate(stateCreator);constinitialState=store.getInitialState();storeResetFns.add(()=>{store.setState(initialState,true);});returnstore;};// when creating a store, we get its initial state, create a reset function and add it in the setexportconstcreate=(<T>(stateCreator:zustand.StateCreator<T>)=>{console.log('zustand create mock');// to support curried version of createreturntypeofstateCreator==='function'?createUncurried(stateCreator):createUncurried;})astypeofzustand.create;constcreateStoreUncurried=<T>(stateCreator:zustand.StateCreator<T>)=>{conststore=actualCreateStore(stateCreator);constinitialState=store.getInitialState();storeResetFns.add(()=>{store.setState(initialState,true);});returnstore;};// when creating a store, we get its initial state, create a reset function and add it in the setexportconstcreateStore=(<T>(stateCreator:zustand.StateCreator<T>)=>{console.log('zustand createStore mock');// to support curried version of createStorereturntypeofstateCreator==='function'?createStoreUncurried(stateCreator):createStoreUncurried;})astypeofzustand.createStore;// reset all stores after each test runafterEach(()=>{act(()=>{storeResetFns.forEach((resetFn)=>{resetFn();});});});
В следующих шагах мы настроим наше окружение Vitest, чтобы выполнить имитацию Zustand.
Предупреждение: В Vitest вы можете изменить root. В связи с этим вам необходимо убедиться, что вы создаете каталог __mocks__ в правильном месте. Допустим, вы изменили root на ./src, это означает, что вам нужно создать директорию __mocks__ в ./src. В итоге получится ./src/__mocks__, а не ./__mocks__. Создание директории __mocks__ в неправильном месте может привести к проблемам при использовании Vitest.
// __mocks__/zustand.tsimport*aszustandfrom'zustand';import{act}from'@testing-library/react';const{create:actualCreate,createStore:actualCreateStore,}=awaitvi.importActual<typeofzustand>('zustand');// a variable to hold reset functions for all stores declared in the appexportconststoreResetFns=newSet<()=>void>();constcreateUncurried=<T>(stateCreator:zustand.StateCreator<T>)=>{conststore=actualCreate(stateCreator);constinitialState=store.getInitialState();storeResetFns.add(()=>{store.setState(initialState,true);});returnstore;};// when creating a store, we get its initial state, create a reset function and add it in the setexportconstcreate=(<T>(stateCreator:zustand.StateCreator<T>)=>{console.log('zustand create mock');// to support curried version of createreturntypeofstateCreator==='function'?createUncurried(stateCreator):createUncurried;})astypeofzustand.create;constcreateStoreUncurried=<T>(stateCreator:zustand.StateCreator<T>)=>{conststore=actualCreateStore(stateCreator);constinitialState=store.getInitialState();storeResetFns.add(()=>{store.setState(initialState,true);});returnstore;};// when creating a store, we get its initial state, create a reset function and add it in the setexportconstcreateStore=(<T>(stateCreator:zustand.StateCreator<T>)=>{console.log('zustand createStore mock');// to support curried version of createStorereturntypeofstateCreator==='function'?createStoreUncurried(stateCreator):createStoreUncurried;})astypeofzustand.createStore;// reset all stores after each test runafterEach(()=>{act(()=>{storeResetFns.forEach((resetFn)=>{resetFn();});});});
Примечание: без включенной globals configuration нам нужно добавить import { afterEach, vi } из 'vitest' в верхней части.
// contexts/use-counter-store-context.tsximport{typeReactNode,createContext,useContext,useRef}from'react'import{createStore}from'zustand'import{useStoreWithEqualityFn}from'zustand/traditional'import{shallow}from'zustand/shallow'import{typeCounterStore,counterStoreCreator,}from'../shared/counter-store-creator'exportconstcreateCounterStore=()=>{returncreateStore<CounterStore>(counterStoreCreator)}exporttypeCounterStoreApi=ReturnType<typeofcreateCounterStore>exportconstCounterStoreContext=createContext<CounterStoreApi|undefined>(undefined,)exportinterfaceCounterStoreProviderProps{children:ReactNode}exportconstCounterStoreProvider=({children,}:CounterStoreProviderProps)=>{constcounterStoreRef=useRef<CounterStoreApi>()if(!counterStoreRef.current){counterStoreRef.current=createCounterStore()}return(<CounterStoreContext.Providervalue={counterStoreRef.current}>{children}</CounterStoreContext.Provider>)}exporttypeUseCounterStoreContextSelector<T>=(store:CounterStore)=>TexportconstuseCounterStoreContext=<T,>(selector:UseCounterStoreContextSelector<T>,):T=>{constcounterStoreContext=useContext(CounterStoreContext)if(counterStoreContext===undefined){thrownewError('useCounterStoreContext must be used within CounterStoreProvider',)}returnuseStoreWithEqualityFn(counterStoreContext,selector,shallow)}
React Testing Library: React Testing Library (RTL) - это очень легковесное решение для тестирования React-компонентов. Она предоставляет вспомогательные функции поверх react-dom и react-dom/test-utils, таким образом, чтобы поощрять лучшие практики тестирования. Его основной руководящий принцип таков: "Чем больше ваши тесты похожи на то, как используется ваше программное обеспечение, тем больше уверенности они могут вам дать".
Нативная библиотека тестирования: Native Testing Library (RNTL) - очень легкое решение для тестирования компонентов React Native, аналогично RTL, но его функции построены поверх react-test-renderer.
Детали реализации тестирования: Заметка в блоге Кента Доддса о том, почему он рекомендует избегать testing implementation details.