Storybookでaxios-mock-adapterを使ってAPIコールをモック

# 背景
Storybookを導入して、動作確認をする時に、API呼び出しを含む処理もあったので、Mockのやり方を確認した。
今回は今まで使い慣れてきたaxios-mock-adapterをStorybookに取り入れる方法を紹介する。
今後、[msw](https://mswjs.io/)も試したいと思う。

# Storybookの導入
### インストール
インストールは下記公式サイトの手順通りに実施する。
https://storybook.js.org/docs/react/get-started/install

### トラブルシューティング
- [このissue](https://github.com/storybookjs/storybook/issues/18298)が出ていたが、`npx storybook init`の一部として、npm7 migrationするかどうか聞かれた場合、[Y]を選択すれば解決される。
- react-scriptsのバージョンに気をつけよう。storybook 6.xを使う場合はreact-scripts 5にバージョンアップする必要がある。でなければ、storybookの起動時に失敗する。

# axios-mock-adapter
今回紹介するサンプルコードを[こちら](https://github.com/coolboy0961/storybook-react)で公開している。

### AxiosMockコンポーネント
```tsx
import { useEffect } from "react";
import MockAdapter from "axios-mock-adapter";
import { axiosInstance } from "../apis/axios-instance";

interface IProps {
  mockApi: (adapter: MockAdapter) => void;
}

const axiosMock = new MockAdapter(axiosInstance);

/**
 * StoryBookのstoriesでaxios mockApi adapterを利用するためのコンポーネント
 * 
 * 
 * Usage:
   export const Default = () => {
    const mockApi = (axiosMock: MockAdapter) => {
      axiosMock.onGet('/api/meetings/1').reply(200, {
        id: 1,
        title: 'A Meeting',
      });
    };
    return (
      <>
        <AxiosMock mockApi={mockApi} />
        <Meeting />
      </>
    );
  };
 * @param mockApi モックの内容を定義するメソッド
 * @see https://gist.github.com/rafaelrozon/ed86efbea53094726e162dc05882cddc
 */
function AxiosMock({ mockApi }: IProps) {
  useEffect(() => {
    mockApi(axiosMock);
    return () => {
      axiosMock.reset();
    };
  }, [mockApi]);
  return <></>;
}

export default AxiosMock;
```

### Customer.stories.tsx
下記コードで示すように、`{mockApi && <AxiosMock mockApi={mockApi} />}`を使ってMockがある時に、AxiosMockコンポーネントを挿入するようにしている。
AxiosMockがレンダリングされると、AxiosMockのuseEffectで定義しているMockの登録処理が実行されるので、APIのMockが登録される。

下記の例では、コンポーネントで手動確認するためのStoryとInteraction Testを実装してある。
```tsx
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { GlobalContextProvider } from "../../contexts/GlobalContext";
import { MemoryRouter } from "react-router-dom";
import { within, userEvent, waitFor } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import Customer from "./Customer";
import MockAdapter from "axios-mock-adapter/types";
import AxiosMock from "../../test-utils/AxiosMock";

export default {
  title: "Pages/Customer",
  component: Customer,
} as ComponentMeta<typeof Customer>;

const Template = (mockApi?: (axiosMock: MockAdapter) => void) => (
  <GlobalContextProvider>
    <MemoryRouter>
      {mockApi && <AxiosMock mockApi={mockApi} />}
      <Customer />
    </MemoryRouter>
  </GlobalContextProvider>
);

export const Default: ComponentStory<typeof Customer> = () => {
  const mockApi = (axiosMock: MockAdapter) => {
    axiosMock.onGet("/api/v1/address?postcode=1840014").reply(200, {
      address: "東京都XXXXXX",
    });
  };
  return Template(mockApi);
};
Default.storyName = "Customerページの手動動作確認";

export const AutoFilledAddress1: ComponentStory<typeof Customer> = () => {
  const mockApi = (axiosMock: MockAdapter) => {
    axiosMock.onGet("/api/v1/address?postcode=1840015").reply(200, {
      address: "東京都XXXXXX",
    });
  };
  return Template(mockApi);
};
AutoFilledAddress1.storyName =
  "郵便番号を入れて、チェックボタンをクリックすると、住所が「住所1」に入ること";
AutoFilledAddress1.play = async ({ canvasElement }: any) => {
  // Arrange
  const expected = "東京都XXXXXX";

  // Act
  const canvas = within(canvasElement);
  const postcodeInputElement = canvas.getByTestId("postcode-input-text");
  userEvent.type(postcodeInputElement, "1840015");
  const addressInputButtonElement = canvas.getByRole("button", {
    name: "住所入力",
  });
  userEvent.click(addressInputButtonElement);

  await waitFor(() => {
    const actual = canvas
      .getByTestId("address1-input-text")
      .getAttribute("value");
    // Assert
    expect(actual).toBe(expected);
  });
};

```

# ハマるポイント
### Mock.onで登録したのに、404 not found
プロダクトコードでAPI呼び出し時に使っているaxios instanceをモックしているか気をつけよう。
例えば、このサンプルコードでaxiosInstanceを作って、API呼び出しをしているのであれば、`const axiosMock = new MockAdapter(axiosInstance);`MockAdapterを作る時もそのinstanceを引数としていれなければならない。`const axiosMock = new MockAdapter(axios);`にしてしまうと、Mockされず、404 not foundになってしまう。
axios-instance.ts
```ts
import axios from "axios";

export const axiosInstance = axios.create();
```

address.ts
```ts
import { axiosInstance } from "./axios-instance";

export default class Address {

  /**
   * BFF の GET api/v1/address APIを呼び出すメソッド
   */
  async getAddress(postcode: string): Promise<AddressResponse> {
    const response = await axiosInstance.get(`/api/v1/address?postcode=${postcode}`);
    return response.data;
  }
}

type AddressResponse = {
  address: string;
};
```