背景
Storybookを導入して、動作確認をする時に、API呼び出しを含む処理もあったので、Mockのやり方を確認した。
今回は今まで使い慣れてきたaxios-mock-adapterをStorybookに取り入れる方法を紹介する。
今後、mswも試したいと思う。
Storybookの導入
インストール
インストールは下記公式サイトの手順通りに実施する。
https://storybook.js.org/docs/react/get-started/install
トラブルシューティング
-
このissueが出ていたが、
npx storybook init
の一部として、npm7 migrationするかどうか聞かれた場合、[Y]を選択すれば解決される。 - react-scriptsのバージョンに気をつけよう。storybook 6.xを使う場合はreact-scripts 5にバージョンアップする必要がある。でなければ、storybookの起動時に失敗する。
axios-mock-adapter
今回紹介するサンプルコードをこちらで公開している。
AxiosMockコンポーネント
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を実装してある。
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
import axios from "axios";
export const axiosInstance = axios.create();
address.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;
};