🧪

QAエンジニアがWebアプリケーションフロントエンドのテストコードを書いてみる(フェイクオブジェクト編)

に公開

ReactなどのWebアプリケーションフレームワークを用いたフロントエンド開発経験ゼロのQAエンジニアが、Webアプリケーションのテストコードを書いてみるシリーズです。

背景として、エンジニアに対して、「テストコードを書いてください」とよくお願いしてしまいます。しかし、エンジニアから「テストコードを書くので書き方を教えてください」と返答されると、それに答えられるスキルがないので、自分なりに簡単なWebアプリケーションのコードとそれを対象としたテストコードを書いてみることにしました。

本書では、フェイクオブジェクトを使ったテストコードを紹介します。

フェイクオブジェクトとは

本節は、xUnit Test PatternsのTest Doubleパターンを引用して、フェイクオブジェクトについて説明します。

フェイクオブジェクトは、テスト実行中に、代替するそのものと本物と同じように振る舞うテストダブルです。フェイクオブジェクトは、間接出力を受け取り、間接入力を操作しますが、あくまでそれも本物と同じように処理したり出力したりします。

間接入力、間接出力については、それぞれ以下の過去の記事を参照してください。


https://zenn.dev/jyoppomu/articles/b7b0f63b2d5ae3


https://zenn.dev/jyoppomu/articles/85db46f4ce106d

フェイクオブジェクトを使ってテストコードを書いてみる

テスト対象が外部依存する部分をフェイクオブジェクトに置き換えて、テストコードを書いてみます。

テスト対象

テスト対象は、以下のような外部のWeb APIをfetchで実行する関数(getImage)です。
この関数は、前述のとおり外部のWebAPIに依存しており、これをテスト対象から切り離すために、Web API自体をフェイクオブジェクトに置き換えます。

async function getImage(apiKey: string | null): Promise<ImageResponse> {
  const url = apiKey
    ? `${baseUrl}?api_key=${apiKey}`
    : `${baseUrl}?api_key=DEMO_KEY`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(
      `Failed to fetch data: ${response.status} ${response.statusText}`
    );
  }
  const image: ImageResponse = await response.json();
  return image;
}

テストコード

テストフレームワークにはVitestを使用します。また、Web APIのフェイクオブジェクトの実装にはMSWを用います。

import {describe, afterEach, afterAll, beforeAll, it, vi} from "vitest";
import {http, HttpResponse} from "msw";
import {setupServer} from "msw/node";

import Api from "@/lib/api";

describe("フェイクオブジェクトを用いたテストコードの例", () => {
  // Web APIをフェイクオブジェクトとして定義する
  const fake = [
    http.get(baseUrl, ({request}) => {
      // Construct a URL instance out of the intercepted request.
      const url = new URL(request.url);
      const apiKey = url.searchParams.get("api_key");
      if (apiKey === "VALID_KEY" || apiKey === "DEMO_KEY") {
        // ...and respond to them using this JSON response.
        return HttpResponse.json(
          {
            copyright: "copyright",
            date: "2025-02-26",
            explanation: "explanation",
            hdurl:
              "url-to-hd-image",
            media_type: "image",
            service_version: "v1",
            title: "This is a title",
            url: "url-to-image"
          },
          {status: 200}
        );
      } else {
        return HttpResponse.json(
          {
            error: {
              code: "API_KEY_INVALID",
              message:
                "An invalid api_key was supplied.",
            },
          },
          {
            status: 403,
          }
        );
      }
    }),
  ];

  const server = setupServer(...fake);

  // テスト実行前にフェイクオブジェクトのサーバーを起動する
  beforeAll(() => server.listen({onUnhandledRequest: "error"}));

  // テスト実行後にフェイクオブジェクトのサーバーを停止する
  afterAll(() => server.close());

  // テストケース間でハンドラをリセットする
  afterEach(() => server.resetHandlers());

  describe("Api.getImage", () => {
    describe("クエリパラメータ: APIキー", () => {
      describe("指定する場合", () => {
        describe("有効なAPIキーの場合", () => {
          it("APODの画像情報の取得が成功する", async ({expect}) => {
            const image = await Api.getImage({key: "VALID_KEY"});
            expect(image).toEqual({
              copyright: "copyright",
              date: "2025-02-26",
              explanation: "explanation",
              hdurl:
                "url-to-hd-image",
              media_type: "image",
              service_version: "v1",
              title: "This is a title",
              url: "url-to-image",
            });
          });
        });
        describe("無効なAPIキーの場合", () => {
          it("APODの画像情報の取得が失敗する", async ({expect}) => {
            await expect(
              Api.getApodImage({key: "INVALID_KEY"})
            ).rejects.toThrowError("Failed to fetch data: 403 Forbidden");
          });
        });
      });
      describe("指定しない場合", () => {
        it("APODの画像情報の取得が成功する", async ({expect}) => {
          const apodImage = await Api.getApodImage();
          expect(apodImage).toEqual({
            copyright: "copyright",
            date: "2025-02-26",
            explanation: "explanation",
            hdurl:
              "url-to-hd-image",
            media_type: "image",
            service_version: "v1",
            title: "This is a title",
            url: "url-to-image",
          });
        });
      });
    });
  });
});

おわりに

フェイクオブジェクトを使って、テスト対象の間接出力を記録するテストコードを書いてみました。
外部のWeb APIを実行する関数をテストする際に、フェイクオブジェクトを使うことで、外部依存を切り離してテストを行うことができました。
これにより、テストの決定性が向上し、テストの信頼性を高めることができます。また、外部APIのレスポンスを自在にコントロールできるため、様々なケースを網羅的にテストすることが可能になります。さらに、ネットワーク通信が発生しないため、テストの実行速度も向上します。

GitHubで編集を提案

Discussion