Как создать макет хранилища в redux toolkit

#reactjs #redux #react-testing-library

#reactjs #redux #react-testing-library

Вопрос:

 import React from 'react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { render, screen, fireEvent } from '@testing-library/react';
import MyApp from './MyApp ';

const initialState = {};
const mockStore = configureStore(initialState);

describe('<MyApp />', () => {
  it('click button and shows modal', () => {
    render(
      <Provider store={mockStore}>
        <MyApp />
      </Provider>
    );

    fireEvent.click(screen.getByText('ADD MIOU'));
    expect(queryByText('Add MIOU Setting')).toBeInTheDocument();
  });
});
 

Я использую jest и redux toolkit reactjs и пытаюсь создать макет хранилища для написания теста.
Но получил следующую ошибку

Ошибка типа: store.getState не является функцией

Есть ли какой-нибудь способ это исправить? Я что-то упустил?

Ответ №1:

Я предполагаю, что вы пытаетесь протестировать подключенный компонент и ожидаете, что (1) создатели и редукторы действий будут запущены и (2) состояние redux будет обновлено как часть вашего теста?

Я не использовал redux-mock-store, но я вижу следующее примечание в их документации, что наводит меня на мысль, что эта библиотека может работать не так, как вы ожидаете:

Пожалуйста, обратите внимание, что эта библиотека предназначена для тестирования логики, связанной с действием, а не с редуктором. Другими словами, он не обновляет хранилище Redux.

Я предлагаю вам попробовать этот подход для тестирования подключенных компонентов. Я использовал этот подход для написания тестов, которые обновляют состояние redux и отображают подключенные компоненты.

Во-первых, вы переопределяете метод RTL render :

 // test-utils.js
import React from 'react'
import { render as rtlRender } from '@testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
// Import your own reducer
import reducer from '../reducer'

function render(
  ui,
  {
    initialState,
    store = createStore(reducer, initialState),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>
  }
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}

// re-export everything
export * from '@testing-library/react'
// override render method
export { render }
 

Затем вы ссылаетесь на этот новый метод рендеринга вместо RTL напрямую. Вы также можете указать начальное состояние для своего теста.

 import React from 'react'
// We're using our own custom render function and not RTL's render
// our custom utils also re-export everything from RTL
// so we can import fireEvent and screen here as well
import { render, fireEvent, screen } from '../../test-utils'
import App from '../../containers/App'

it('Renders the connected app with initialState', () => {
  render(<App />, { initialState: { user: 'Redux User' } })

  expect(screen.getByText(/redux user/i)).toBeInTheDocument()
})
 

(Весь код скопирован из redux.js.org .)

Комментарии:

1. Я пытаюсь сделать это в typescript, однако я не могу найти правильные типы для использования test-utils.js досье. Вы пробовали это в typescript?

Ответ №2:

У меня была та же проблема, что и у вас, но благодаря @srk за ссылку на документы Redux и документы библиотеки тестирования React я нашел довольно хорошее решение, которое сработало для меня с TypeScript:

 // store.ts - just for better understanding
export const store = configureStore({
  reducer: { user: userReducer },
})

export type RootState = ReturnType<typeof store.getState>
 
 // test-utils.ts
import React, { ReactElement } from 'react'
import { Provider } from 'react-redux'
import { render as rtlRender, RenderOptions } from '@testing-library/react'
import {
  configureStore,
  EmptyObject,
  EnhancedStore,
  PreloadedState,
} from '@reduxjs/toolkit'

// import your reducers
import userReducer from 'features/user/user.slice'

import type { RootState } from 'app/store'

// ReducerTypes is just a grouping of each slice type,
// in this example i'm just passing down a User Reducer/State.
// With this, you can define the type for your store.
// The type of a configureStore() is called EnhancedStore,
// which in turn receives the store state as a generic (the same from store.getState()).
type ReducerTypes = Pick<RootState, 'user'>
type TStore = EnhancedStore<ReducerTypes>

type CustomRenderOptions = {
  preloadedState?: PreloadedState<ReducerTypes amp; EmptyObject>
  store?: TStore
} amp; Omit<RenderOptions, 'wrapper'>

function render(ui: ReactElement, options?: CustomRenderOptions) {
  const { preloadedState } = options || {}

  const store =
    options?.store ||
    configureStore({
      reducer: {
        user: userReducer,
      },
      preloadedState,
    })

  function Wrapper({ children }: { children: React.ReactNode }) {
    return <Provider store={store}>{children}</Provider>
  }

  return rtlRender(ui, { wrapper: Wrapper, ...options })
}

// re-export everything
export * from '@testing-library/react'
// override render method
export { render }
 

Затем вам просто нужно передать объект со свойством preloadedState в качестве второго параметра для вашего рендеринга; вы даже можете определить новое хранилище внутри рендеринга, если хотите, со свойством «store».

 describe('[Component] Home', () => {
  it('User not logged', () => {
    const component = render(<Home />)
    expect(component.getByText(/User is: undefined/i)).toBeInTheDocument()
  })

  it('User logged in', () => {
    const component = render(<Home />, {
      preloadedState: { user: { name: 'John' /* ...other user stuff */ } },
    })
    expect(component.getByText(/User is: John/i)).toBeInTheDocument()
  })
})
 

Ответ №3:

Я не мог найти где-либо еще, чтобы вставить свои выводы относительно redux toolkit и redux-mock-store.

Чтобы отправлять асинхронные запросы и результаты тестирования, вы можете указать тип отправки при создании макета хранилища.

 import configureStore from 'redux-mock-store';
import { getDefaultMiddleware } from '@reduxjs/toolkit'

const middlewares = getDefaultMiddleware();
const mockStore = createMockStore<IRootState, AppDispatch>(middlewares);

describe('my thunk action', () => {
  const store = mockStore();

  test('calls my service', async() => {
    await store.dispatch(myThunk({ id: 32 }));

    expect(myService).toBeCalledWith({ id: 32 });
  });

  test('contains foo bar actions', async() => {
    await store.dispatch(myThunk({ id: 32 }));

    expect(store.getActions()).toEqual(....);
  });
});