yikegaya’s blog

仕事関連(Webエンジニア)について書いてます

React/Redux環境へのStorybook、テストコード導入で時間がかかった作業メモ

仕事でReact、Next.js、Redux製プロダクトにStorybookとtesting-library/react、vitestを使ったテストコードの導入作業を担当しました。その際に時間がかかった作業と解決方法をメモります。

  • ReduxのStoreで管理されたstateを使ったコンポーネントのStorybook実装
  • Reduxを使ったコンポーネントのStorybook表示
  • 非同期でバックエンドからデータを取得するテストの実装
  • vitestで実装したテストのmockが通らない問題の解消
  • Redux Storeで管理されているstateのmock化

ReduxのStoreで管理されたstateを使ったコンポーネントのStorybook実装

ReduxのStoreで管理されたstateを保持するコンポーネントにStorybookを追加する場合モックのStoreを定義してそのモックを親コンポーネントとしてrenderする必要があるみたいです。

storybook.js.org

const Mockstore = ({ taskboxState, children }) => (
  <Provider
    store={configureStore({
      reducer: {
        taskbox: createSlice({
          name: 'taskbox',
          initialState: taskboxState,
          reducers: {
            updateTaskState: (state, action) => {
              const { id, newTaskState } = action.payload;
              const task = state.tasks.findIndex((task) => task.id === id);
              if (task >= 0) {
                state.tasks[task].state = newTaskState;
              }
            },
          },
        }).reducer,
      },
    })}
  >
    {children}
  </Provider>
)

非同期でバックエンドからデータを取得するテストの実装

こんな感じで画面表示時にuseEffectから非同期でaxiosを使ってバックエンドからデータを取得するコンポーネントがあったんですがそのテストが通らずしばらくハマりました。

useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get(

expect文をawait waitForで囲んで非同期での取得が完了するのを待つことによってパスしました。

  it('一覧が正しく表示されること', async () => {
    renderWithStore(<AdvertisersList />)

    await waitFor(() => {
      expect(screen.getByText('テスト1')).toBeInTheDocument()
      expect(screen.getByText('テスト2')).toBeInTheDocument()
    })
  })

vitestで実装したテストのmockが通らない問題の解消

なぜかReduxを使ったレンダリングテスト用の関数ファイルにmockを仕込んでしまってその関数を使ったテストファイルでのmockが打ち消されてしまい解決にしばらく時間かかりました。

// tests/test-utils.tsx
import { render } from '@testing-library/react'
import StoreProvider from '../app/StoreProvider'

export const renderWithStore = (component: React.ReactNode) => {
  return render(<StoreProvider>{component}</StoreProvider>)
}

上記ファイルになぜかmockを仕込んでしまったんですがそうするとrenderWithStoreを使ったファイルでのmock定義が打ち消されます。 標準出力のメッセージを読んでも理由がわからずしばらくハマりました。

// tests/test-utils.tsx
import { render } from '@testing-library/react'
import StoreProvider from '../app/StoreProvider'

// こうした場合renderWithStoreを使ったテストファイルでnext/navigationのmockを書いても有効にならない
vi.mock('next/navigation', () => ({
  useRouter: vi.fn()
}))

export const renderWithStore = (component: React.ReactNode) => {
  return render(<StoreProvider>{component}</StoreProvider>)
}

Redux Storeで管理されているstateのmock化

こんな感じでstate、メソッドをmockしました。

vi.mock('react-redux', () => ({
  useSelector: vi.fn(),
  useDispatch: () => mockDispatch,
  Provider: ({ children }: { children: React.ReactNode }) => children
}))

describe('Toast Component', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('デフォルトの状態で正しくレンダリングされること', () => {
    const mockState = {
      layout: {
        showToast: true,
        toastState: {
          severity: 'success',
          label: 'テストメッセージ'
        }
      }
    }
    vi.mocked(useSelector).mockImplementation(selector => selector(mockState))

    renderWithStore(<Toast />)