💯

React + Storybook + MSW + Vitestで作る堅牢なフロントエンド開発術

2024/09/20に公開

https://www.youtube.com/watch?v=6k_D7R2hH2o

概要

フロントエンド開発において、コンポーネントの開発、カタログ化、テストは非常に重要ですが、APIリクエストを伴うコンポーネントの場合、これらのプロセスが複雑になることがあります。この動画では、React、Storybook、MSW(Mock Service Worker)、Vitestを組み合わせることで、効率的かつ堅牢なフロントエンド開発アプローチを実現する方法を紹介します。

想定する読者・視聴者

以下のような読者・視聴者を想定しています。この記事は(文量が多くなってしまうので)細部の話までは触れておらず、概要のみ記載しています。詳細に関しては各セクション毎に対応する動画を視聴いただけますと幸いです。

  • レガシーなアプリにStorybookを導入したい
  • Storybookがあるので、Storyに対してテストを書きたい
  • Storybook + MSW + Vitest(テストフレームワーク)の関係性を知りたい

ライブラリの概要

使用する主要なツールは以下の通りです(前提としてReactを使います):

  • Storybook: コンポーネントのカタログ化と開発
  • MSW (Mock Service Worker): APIリクエストのモック
  • Vitest: 高速なユニットテストとコンポーネントテスト

これらのツールを連携させることで、APIに依存するコンポーネントの開発やテストを効率的に行うことができます。

Formの作成

動画へ

APIリクエストを行う簡単なフォームをまずは作成して、課題を確認します。

Storybookの導入

動画へ

Storybookを導入し、アプリ側とは独立してコンポーネントを開発します。

MSWをStorybookに導入

動画へ

コンポーネント内でAPIをリクエストしている場合、StorybookではAPIへのリクエストが失敗するせいでうまくコンポーネントがレンダリングできないことがあります。そこで登場するのがMSW(Mock Service Worker)

https://mswjs.io/

MSWはネットワークレベルでリクエストをモックするため、コンポーネントからは通常通りAPIへのリクエストを行っているにもかかわらず、モックレスポンスを返せるという強者です

このモックのおかげで、APIのリクエストを差し替えたり、APIを事前に用意したり、という手間をかけずに済むのです。

ここまでの実装で、ReactコンポーネントがAPIリクエストを行い、Storybookで表示され、MSWでモックされたデータを使用できるようになりました。

StorybookでFormの動作確認(テスト)

動画へ

Storybookを使って、作成したフォームコンポーネントの動作確認を、アプリとは独立して行えます。

しかし、その場での確認としてはこれで良しとして、中長期で見た場合にいちいち動作確認をし続けるのは効率がよくありません。できればテストは自動で行っておいて品質を担保しておきたいものです

Vitestの導入

動画へ

そこで次に導入するのがVitestとReact Testing Libraryです。

https://vitest.dev/

https://testing-library.com/docs/react-testing-library/intro/

ここではVitestを導入していますが、Jestなど他のテストフレームワークでも問題はありません(ただし、Jestでは若干導入に苦労するポイントがあります)。

流れとしては:

  1. VitestからStorybookのコンポーネントをimport
  2. コンポーネントのテストをReact Testing Libraryを活用しつつ実施

という感じです。

VitestからReactのコンポーネントを直接importせず、Storybookのコンポーネントをimportする理由は「Storybookであらかじめコンポーネントの状態が準備されている」ためです。

多くの場合Storybookではカタログ化しておきたいコンポーネントの状態が準備してあります。Reactコンポーネントを直接importした場合、どのみちこの状態を準備することになるため、冗長な作業が発生してしまいます。

ということで、「それだったらStorybookのコンポーネントをテストすればいいじゃん」という結論にたどり着きました。

またVitestからStorybookで使用しているMSWのハンドラーも流用することで、APIリクエストの状態に関してもStorybookと同様なものを再現することができます。

VitestにおけるMSWの問題とBoundaryの活用

動画へ

VitestでMSWを使うにあたって問題があります。それは、MSWのサーバーインスタンスが一つなのに対して、並列でテストが走ってしまうとモックのハンドラーがテスト毎に独立しない点です。

例えば以下のようなテストがあったとします:

  • Test A + モックハンドラーA(エンドポイントA)
  • Test B + モックハンドラーB(同じくエンドポイントA)

このTest AとTest Bが並列で実行された場合、MSWがどのモックハンドラーを使うのかが不定となってしまいます(インスタンスが一つしか無いため)。

そこで2024年に登場したのがBoundaryという機能です。

https://mswjs.io/blog/introducing-server-boundary/

これがかなりの神機能になっており、テスト毎に独立したモックサーバーの状態を活用できるようになったのです。

テストの仕上げ

動画へ

最終的にテストは以下のようになります。

import { test, expect, describe, vi, beforeAll, afterAll } from 'vitest';
import { composeStory } from '@storybook/react';
import { render, screen, waitFor } from '@testing-library/react';
import Meta, * as Stories from './Form.stories';
import { userEvent } from '@storybook/test';
import { setupServer } from 'msw/node';

const server = setupServer();

beforeAll(() => server.listen());

afterAll(() => server.close());

describe('Form', () => {
  test.concurrent(
    'should submit correct data',
    // boundaryによってMSWはテスト毎の独立した空間を作成
    server.boundary(async () => {
      server.use(...Stories.mockHandler);
      // Arrange
      const expectedSelectedValue = 'option-c';
      const submitHandler = vi.fn();

      // StorybookのコンポーネントをReactコンポーネントに変換
      const Form = composeStory(Stories.Default, Meta);
      render(<Form onSubmit={submitHandler} />);

      await waitFor(() => {
        // APIから取得してきたOptionが表示されるまで待つ
        const options = screen.getAllByRole('option');
        expect(options).toHaveLength(3);
      });

      // Selectの値を選択する
      const combobox = screen.getByRole('combobox');
      await userEvent.selectOptions(combobox, expectedSelectedValue);


      const submitButton = screen.getByRole('button', { name: 'Submit' });

      // Act
      // Formを送信する
      await userEvent.click(submitButton);

      // Assert
      // 期待した値がFormから送信したか確認する
      await expect(submitHandler).toHaveBeenCalledWith({
        option: expectedSelectedValue,
      });
    })
  );
});

まとめ

React、Storybook、MSW、Vitestを連携させることで、以下のような利点が得られます:

GG1. コンポーネントの独立した開発とカタログ化
G2. API依存のコンポーネントの効率的なテスト
3. 開発環境とテスト環境での一貫したデータ使用
4. 非同期処理を含むコンポーネントの堅牢なテスト

このアプローチを採用することで、フロントエンド開発の効率と品質を大きく向上させることができます。ただし、MSWの設定や非同期テストの管理など、一定の学習コストがかかる点に注意は必要です。

このアプローチを自身のプロジェクトに取り入れ、効率的で堅牢なフロントエンド開発を実現してみてください!

今回使用しているコードは以下に格納しています。

https://github.com/craftell/study-react-storybook-msw-vitest

おまけ

Storybookにも実はInteraction Testという機能があります。

https://storybook.js.org/docs/6/writing-tests/interaction-testing

これを活用することでStorybook内でテストをすることも可能です。Chromaticと組み合わせることで自動テストもできます。ただし、個人的にはこのやり方にはまだ課題を感じており、VitestやJestでテストを高速に回す方が好きです。

もし興味がある方は、以下の動画で実際にInteraction Testも実装しているので、合わせて見てみてください!
https://www.youtube.com/watch?v=o3UNkjwDyYE

Discussion