実践編 Storybookでのテスト [5日間でReactの基礎を習得したい 5/5日目]

# はじめに

5日間でReactの基礎を習得する試みの5日目です。最終日は、[4日目](https://qiita.com/MichaelHashimoto/items/8c4ce6e8df40db9488df)に作成したNavLinkコンポーネントのテストをStorybookでやってみます。

https://qiita.com/MichaelHashimoto/items/f3ad9f56081a40b9b788

# Storybook

## Storybookのインストール

前回作成したプロジェクトディレクトリで以下のコマンドを実行すると、StoryBookがインストールされ、起動します。
```
npx storybook@latest init
```

私の場合は、依存関係の問題でうまく行かなかったため、次のようにしました。
```
npm install --save react-rough-notation@1.0.5  // 依存関係のエラーを解決するため、react-rough-notationをバージョンアップした。
npx storybook@latest init
```

Storynbookの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/8ba853fa-272a-9deb-f4dc-fc39971dfe58.png)

storiesというディレクトリとファイル群が追加されており、ここの*.stories.jsに記載された内容にしたがって、サイドバーにメニューが表示されます。初期状態では、Button, Header, Pageというサンプルがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/098b7031-7ff8-25fc-8d9b-2234ccd53c38.png)



## storyの作成

[前回作成したNavLinkコンポーネント](https://qiita.com/MichaelHashimoto/items/8c4ce6e8df40db9488df#%E3%83%8A%E3%83%93%E3%82%B2%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%90%E3%83%BC%E3%82%92%E7%B7%A8%E9%9B%86%E3%81%99%E3%82%8B)に対し、storyを作成します。

svgにdata-testidを付与しています。
```jsx
export const NavLink = ({ href, children, icon}) => {
  const router = useRouter();
  const isActive = router.asPath === href;

  const className = isActive
    ? "text-gray-800 font-bold dark:text-gray-400"
    : "text-gray-600 dark:text-gray-300 font-normal";

  return (
    <Link href={href} className={`text-base ${className}`}>
      <Button color="secondary">
      {icon}
        <Typography variant="body1">
          {children}
        </Typography>
      </Button>
      {isActive && (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          fill="currentColor"
          className="bi bi-arrow-down inline-block h-3 w-3"
          viewBox="0 0 16 16"
          data-testid="svg-arrow-down" // テストのために追加
        >
          <path fillRule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z" />
        </svg>
      )}
    </Link>
  );
};
```

stories/NavLink.stories.jsというファイルを作ります。

```jsx:NavLink.stories.js
import { within, userEvent, expect } from '@storybook/test';
import {NavLink} from '@components/Navbar';

export default {
  title: 'Example/NavLink',
  component: NavLink,
  parameters: {
    // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
    layout: 'centered',
  },
};


// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing
export const Test1 = {
    args: {href:'/example', children:'TEST1', icon: <Info />},
};

// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing
export const Test2 = {
    args: {href:'/example', children:'TEST2'},
};
```

### 解説

storiesディレクトリには、Button.storiese.jsなどのサンプルコードが存在するため、それをベースにし、適宜公式HPを見ながらファイルを作ります。

まず、Storybookでのテスト・表示対象のコンポーネントをインポートします。
```jsx
// "@components"で指定されるディレクトリにあるNavbar.jsからNavLinkコンポーネントをインポート
// "@components"が指すパスはjsconfig.jsonを見ればわかる
import {NavLink} from '@components/Navbar';
// propsで使うIcon
import {Info} from "@mui/icons-material";
```

Storybookの作法に従い、defaultを設定します。
```jsx
export default {
  title: 'Example/NavLink', // メニューのタイトル。この場合メニューのExamle=>NavLink下に表示される
  component: NavLink,  // 対象のコンポーネント
  parameters: {  // 画面表示のためにStorybookが提供するパラメータ。一つだけ設定した。
    layout: 'centered',
  },
};
```

NavLinkのpropsを、表示させたい数だけ設定します。NavLinkの引数はhref,children,iconなので、以下のようになります。
```jsx
export const Test1 = {
    args: {href:'/example', children:'TEST1', icon: <Info />},
};

export const Test2 = {
    args: {href:'/example', children:'TEST2'},
};
```

### 表示される画面

Test1, Test2の２つを宣言したため、メニューバーにはそれぞれ表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/e37a4981-c31e-dd92-9ce7-67aacb6b4aeb.png)

Test1のページは次のような感じで表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/92232e05-6b87-fc5c-68a0-dc06896689d3.png)

## Interaction Test

テストコードを書きます。

NavLinkコンポーネントは、ルーターを使っており、hrefのページに移ったときは、右側に"↓"が表示され、他のページでは表示されないものでした。(hrefが"/example"なら、{URL}/exampleのページに移動した時だけ、"↓"が表示される。)
これをテストすることにします。

#### hrefのページ以外にいるときのパターン

Test1にテストコードを追記します。
```jsx
export const Test1 = {
  args: {
    href: '/example',
    children: 'Test1',
    icon: <Info />
  },
  
  // 追加
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    const link = canvas.getByRole('link', { name: 'Test1' });
    await userEvent.click(link);

    // SVG要素が存在しないことを確認
    await expect(canvas.getByTestId('svg-arrow-down')).not.toBeInTheDocument();
  }
};
```

##### 解説
Storybookでは、play関数を実装しておけば、Storybookにコンポーネントが描画されたときに自動で実行してくれます。つまり、ここにテストコードを書きます。
```jsx
play: async ({ canvasElement }) => {
    // canvasを使って表示されているHTMLの要素を検索し、取得できるようにする
    const canvas = within(canvasElement);

    // リンクの取得、クリック
    const link = canvas.getByRole('link', { name: 'Test1' });  // アクセシビリティロールが"link"のもので、かつ名前が"Test1"であるものを取得
    await userEvent.click(link);
    
    // 判定部分。SVG要素が存在しないことを確認
    await expect(canvas.queryByTestId('svg-arrow-down')).not.toBeInTheDocument();
}
```
取得する要素のアクセシビリティロールは、ブラウザのF12から開発者ツールを開き、調べました。
他に方法があるのかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/1b5687e4-ebfa-6959-9d2f-8f1f780a66f4.png)

上の方で記載したとおり、"↓"を描画するSVG要素には、あらかじめ"svg-arrow-down"というdata-testidを付与していました。
そのため、表示されていなければ、queryByTestIdで要素を取得できないはずです。それをexpectで判定しています。ちなみに、getByTestIdを使うと、要素が取得出来ない場合はエラーが出るため、テストできなくなります。

#### hrefのページにいるときのパターン

Test2にテストコードを追記します。
前回勉強したように、routerオブジェクトを使って、表示中のパスを取得しています。しかし、Storybookではパスの移動ができないため、parametersを使って、移動したときの状態を再現しています。
```jsx
export const Test2 = {
  args: {
    href: '/example',
    children: 'Test2',
    icon: <Info />
  },

  // parametersを使って、ルーターを設定。
  parameters: {
    nextjs: {
      router: {
        pathname: '',
        asPath: '/example',
        query: {
          id: '',
        },
      },
    },
  },
  
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    const link = canvas.getByRole('link', { name: 'Test2' });
    await userEvent.click(link);

    // 判定部分。SVG要素が存在することを確認
    await expect(canvas.getByTestId('svg-arrow-down')).toBeInTheDocument();
  }
};
```

paramters部分のコードは、[公式HPのNext.jsの説明部分](https://storybook.js.org/docs/get-started/nextjs#nextjs-routing)を参考にしました。

##### 解説

ほとんどTest1と同じですが、SVG要素が存在"する"ことの確認なので、```expect().toBeInTheDocument```となっています。

#### テストの結果

Storybookで、"Interactions"のタブをチェックしましょう。
判定結果が出ています。どちらも合格です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/0c403e19-280e-1e7a-2c4b-14017a126b39.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1223968/9b760af0-b91d-c081-6809-3ac08acac18f.png)


#### ちなみに

他の判定を行うコードは、以下の記事で紹介されています。

https://qiita.com/keitaMax/items/64b3956775ac83959bdf

TestはCLIで実行することもできそうです。

https://qiita.com/keitaMax/items/66e46cdf3541bc1a703d

# 最後に

Storybookは高機能であり、今回触れられた部分はほんの少しです。本当は、storyのファイルを置く場所、ディレクトリの構造、メニューの構造を検討しないといけないと思います。

とりあえず、5日間でざっくりとReactの基礎を習得することができました。これらの知識を応用していけば、Reaceを使ったWebアプリケーションが作れるようになるのだと思います。

本記事がどなたかのReact基礎の理解の助けになっていれば幸いです。

何か間違いがあれば、是非コメントを頂けると助かります！

# 参考文献

https://qiita.com/k_kuroiwa/items/cdf9fdafb044a68eebb1