📚

テスト嫌いによるvitest事始め

2024/01/08に公開3

はじめに

テストコードを書くのを食わず嫌いしていたのですが、心境や状況の変化があったのでこれを機に書いてみることにしました。

なぜテストを書かなかったか

  • テストコードを書く習慣がなかった
  • 人力テストで網羅できていると思っていた
  • テストコードの書き方のノウハウがなかった
  • 試験コードの工数が実装コードの工数と同等かそれ以上になりそうだったのでやっていなかった

なぜ今頃になってテストを書こうと思ったのか

  • Pull Request をレビューしてもらう立場(レビュイー)からレビューする側(レビュアー)になった
  • 人力テストで網羅していると思っても要件やパターンが抜け落ちていることがあった
  • 趣味での個人開発でも考慮漏れが発生し、目に見える形(coverage)で網羅する必要があると思った
  • ちょうど新しいプロジェクトの立ち上がりフェーズで、自動テストを導入するのであれば一通り調査する必要があった
  • 試験工数はあまり計上できてないが、後で泣きを見るのは自分なので今のうちに壁にぶち当たっていた方がよいと思った

この記事の前提条件

  • NextJS 14
  • React 18
  • Typescript 5

インストールするライブラリ

    "@testing-library/jest-dom": "^6.1.5",
    "@testing-library/react": "^14.1.2",
    "@vitejs/plugin-react": "^4.2.1",
    "jsdom": "^23.0.1",
    "vitest": "^1.1.0",
    "@testing-library/user-event":"^14.5.1",
    "@vitest/coverage-v8": "^1.1.0"

コンポーネントテスト

例として、placeholder を外部から渡せて、入力値を外部(store など)に渡せるコンポーネントのテストをする。

  • placeholder を正常に渡せているか、該当の文字列を描画した要素があるかテストする
  • 入力した値を外部に渡す処理が動作しているか、mock した関数(setter)に想定した値が渡っていることを検証する
import "@testing-library/jest-dom";
import { render, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, vi, describe, beforeEach } from "vitest";
import { JSX } from "react";

// 処理を共通化
export function setup(jsx: JSX.Element) {
  return {
    user: userEvent.setup(),
    ...render(jsx),
  };
}

describe("InputText", () => {
  // テストの都度最初にmockを初期化する
  beforeEach(() => {
    vi.clearAllMocks();
  });

  const setter = vi.fn();
  const props: Props = {
    placeholder: "テストテキスト",
    setter,
  };
  const { user } = setup(<InputText {...props} />);

  test("プレースホルダーテスト", () => {
    const { container } = render(<InputText {...props} />);
    const element = container.querySelector(
      "input[placeholder=テストテキスト]"
    );
    expect(element).toBeTruthy();
  });

  test("入力するとsetterにイベントが渡されること", async () => {
    const { container } = render(<InputText {...props} />);
    const element = container.querySelector(
      "input[placeholder=テストテキスト]"
    );
    if (element) {
      await user.type(element, "sample@example.com");
    }
    await waitFor(() => {
      expect(setter).toHaveBeenCalledWith("sample@example.com");
    });
  });

  test("snapshot test", () => {
    const { container } = render(<InputText {...props} />);
    const element = container.querySelector(
      "input[placeholder=プレースホルダーテスト]"
    );
    expect(element).toMatchSnapshot();
  });
});

結合テスト(ページ単位テスト)

store にnanostoresを使っているのですが、(タイムラグがあるためか)すぐに状態が更新されず waitFor の中にさらに待ち時間を入れてやることで解決した例です。

test("チェックボックスにチェックを入れるとボタンが押せるようになること", async () => {
  render(<SamplePage {...props} />);
  const checkBox = document.querySelector("hogehoge");
  if (checkBox) {
    await waitFor(() => {
      // チェックボックスが存在すればクリック
      user.click(checkBox);
    });
  }
  const button = document.querySelectorAll(buttonSelector);
  await waitFor(() => {
    setTimeout(() => {
      expect(button?.[0]).toBeEnabled();
    }, 1000);
  });
});

サーバからのレスポンスを mock してそれに応じた振る舞いができているかどうかを確認します。以下は 400 の例ですが、他のステータスコードに応用することもできます。

test("不正なリクエストの場合エラーメッセージが表示されること", async () => {
  let mockedFetch: MockInstance<
    [input: string | URL | Request, init?: RequestInit | undefined],
    Promise<Response>
  > = vi
    .spyOn(global, "fetch")
    .mockImplementation(async () => await new Response("", { status: 400 }));
  render(<SamplePage {...props} />);
  if (button) {
    await user.click(button);
  }
  await waitFor(() => {
    expect(mockedFetch).toHaveBeenCalledOnce();
    expect(screen.findAllByText("エラーメッセージ")).toBeTruthy();
  });
});

参考文献

https://de-milestones.com/jest-react-testing-library-form-test/
https://zenn.dev/geb/articles/230225_vitest_fetch_mock

Discussion

たつたつ

すみません!この記事とは関係無いのですが、ドラフトなう!の画面作成のサイトが入れなくなっています!

koshienkoshien

申し訳ありません。ドラフトなうのサービスは閉めました。

たつたつ

そうなんですか…結構使わせて頂いてたので残念です。