はじめに
こんにちは!開発チームの遠藤です。  
MRC(Markerise Cloud)では、現在フロントエンドのリプレイスが行われており、   
テストコードを書きながら開発しています。  
本記事では、実際に React コンポーネントをどのような観点で、
テストコードを書いているのかを、簡単にですが、紹介したいと思います。
環境
- react: v17.0.2
- react-dom: v17.0.1
- @testing-library/react: v11.1.2
- @testing-library/react-hooks: v3.4.2
テスト観点
テストの観点や粒度は、プロジェクトによって様々ですが、
フロントエンド開発では、以下の種別でテストコードを書いています。
- コンポーネント
- カスタムフック
- 関数
何をチェックしているのか
- コンポーネントテスト-  DOM を参照して見た目をチェック- コンポーネントがレンダリングされたとき
- ユーザーイベントが発火されたとき e.g.
 
 
-  DOM を参照して見た目をチェック
- カスタムフックテスト- 状態と更新関数の紐づきをチェック
 
- 状態と更新関数の紐づきをチェック
- 関数テスト- 関数内の処理をチェック
 
コード例 (コンポーネント)
実際にコンポーネントの例を書いてみました。(あくまでも例なのでご了承ください)  
機能としては、ファイル選択を行うと選択されたファイルのサイズを byte ~ TB の単位付きで表示するものです。
コンポーネント
type Testable = { "data-testid"?: string | undefined };
export const FileSizeCheck: VFC<Testable> = ({ "data-testid": testid }) => {
  const [file, setFile] = useInputFileState();
  const fileSize = useMemo(
    () => file && convertFileSize(file.size).join(""),
    [file]
  );
  return (
    <div data-testid={testid}>
      <input type="file" onChange={setFile} />
      {fileSize ? <div>{`Size: ${fileSize}`}</div> : <></>}
    </div>
  );
};テスト種別に当てはめてみる
上のコードをテスト種別に当てはめてみると、次のようになります。
- コンポーネント →- カスタムフック → useInputFileStateが対象
- 関数 → convertFileSizeが対象
コード例 (カスタムフック・関数)
以下、コンポーネント内で使用しているコードになります。
カスタムフック
export const useInputFileState = (): [
  File | undefined,
  Dispatch<ChangeEvent<HTMLInputElement>>
] => {
  const [value, setValue] = useState<File>();
  return [value, getInputOnChange(setValue)];
};
const getInputOnChange =
  (setValue: Dispatch<File>) => (event: ChangeEvent<HTMLInputElement>) => {
    if (!event.target.files?.length) return;
    const file = event.target.files[0];
    setValue(file);
  };関数
type FileSizeUnit = "TB" | "GB" | "MB" | "KB" | "byte";
export const convertFileSize = (size: number): [number, FileSizeUnit] => {
  if (size >= 1024 ** 4) return [calc(size, 4), "TB"];
  if (size >= 1024 ** 3) return [calc(size, 3), "GB"];
  if (size >= 1024 ** 2) return [calc(size, 2), "MB"];
  if (size >= 1024 ** 1) return [calc(size, 1), "KB"];
  return [size, "byte"];
};
const calc = (size: number, n: number) =>
  Math.floor((size / 1024 ** n) * 100) / 100;テストコードを書いてみる
次にテストコードを書いてみます。
実際には、仕様に沿ってきちんとテスト項目の洗い出しに加え、同値分割・境界値分析等を用いて、
適切なテストデータを作成する必要がありますが、割愛させて頂きます。
コンポーネントテスト
const fileMock = new File(["test"], "test.csv");
describe("FileSizeCheck", () => {
  test("ファイルを選択するとファイルのサイズが表示される", () => {
    const { getByTestId, queryByText } = render(
      <FileSizeCheck data-testid="file-size-check" />
    );
    const component = getByTestId("file-size-check");
    expect(queryByText("Size: 4byte")).toBeFalsy();
    const input = component.getElementsByTagName("input")[0];
    fireEvent.change(input, { target: { files: [fileMock] } });
    const file = input.files && input.files[0];
    expect(file).toBeTruthy();
    expect(file?.name).toBe("test.csv");
    expect(queryByText("Size: 4byte")).toBeTruthy();
  });
});カスタムフックテスト
const changeEventFileMock = {
  target: { files: [fileMock] },
} as unknown as ChangeEvent<HTMLInputElement>;
const changeEventEmptyFileMock = {
  target: { files: [] },
} as unknown as ChangeEvent<HTMLInputElement>;
describe("useInputFileState", () => {
  test("stateの初期値はundefinedである", () => {
    const { result } = renderHook(() => useInputFileState());
    const [value] = result.current;
    expect(value).toBeUndefined();
  });
  test("changeイベント(Fileあり)を受け取るとstateにFileがセットされる", () => {
    const { result } = renderHook(() => useInputFileState());
    const [, setValue] = result.current;
    act(() => setValue(changeEventFileMock));
    const [value] = result.current;
    expect(value).toBe(fileMock);
  });
  test("changeイベント(Fileなし)を受け取ってもstateは変化しない", () => {
    const { result } = renderHook(() => useInputFileState());
    const [, setValue] = result.current;
    act(() => setValue(changeEventFileMock))
    act(() => setValue(changeEventEmptyFileMock));
    const [value] = result.current;
    expect(value).toBe(fileMock);
  });
});関数テスト
describe("convertFileSize", () => {
  test(`引数 1024 ** 4 の戻り値は [1, "TB"] である`, () => {
    const result = convertFileSize(1024 ** 4);
    expect(result).toEqual([1, "TB"]);
  });
  test(`引数 1024 ** 3 の戻り値は [1, "GB"] である`, () => {
    const result = convertFileSize(1024 ** 3);
    expect(result).toEqual([1, "GB"]);
  });
  test(`引数 1024 ** 2 の戻り値は [1, "MB"] である`, () => {
    const result = convertFileSize(1024 ** 2);
    expect(result).toEqual([1, "MB"]);
  });
  test(`引数 1024 ** 1 の戻り値は [1, "KB"] である`, () => {
    const result = convertFileSize(1024 ** 1);
    expect(result).toEqual([1, "KB"]);
  });
  test(`引数 1024 - 1 の戻り値は [1023, "byte"] である`, () => {
    const result = convertFileSize(1024 - 1);
    expect(result).toEqual([1023, "byte"]);
  });
});終わりに
テストコードを書いていれば、実装時の不具合チェックだけでなく、既存コードへの影響も検知できるので、コードの品質向上が期待できます!
テストで検証できていない実データやユーザー操作が存在する可能性があるので、
私は、テストコードを書いていれば完璧とは考えずに、あくまでもテストと思いながら、
テストコードを実装しています!
また、Testing Library は実際のブラウザ上には描画していないので、注意が必要だと考えています。
E2E テストでは、よりユーザー操作に近い形でテストができるので、テスト要件によっては E2E も選択肢に入ると思います。
Playwrightでもv1.22.0から コンポーネントテストもできるようになったので、こちらもチェックして記事にしてきたいと思います。
以上、参考になりましたら幸いです。
 
         
         
        
       
              
            