💯

Reactのテストコードの問題を作ってみた [初心者向け]

2023/12/11に公開

React問題

ログインフォームのテストコードを書くと結構勉強になったので問題形式にしてまとめました。

もしよければやってみてください。

最初の解説

始めにいくつかの点について解説をしておきます。最初から全て読む必要はありません。 適宜必要になれば戻って見返す、という程度で十分です。
解説は最初にある、と明確にするために最初に書いてます。

解説は後でいいから問題へ

以下解説です

JestとReact Testing Libraryのどっちを使うの?

以下、React Testing LibraryをRTLとして説明します。

これは自分が勘違いしていたことですが、初め
「JestとRTLどっちを使うほうがいいんだろう?」 
って思ってました。
しかし、結論は 「両方必要」
JestはJavaScriptのためのテストフレームワークで、RTLはReactのコンポーネントテストのためのライブラリ。目的が違います。
運転免許試験で例えると、Jestは試験会場や筆記試験、発表方法などを広範囲のテストに必要なものを準備する。それに対してRTLは実技試験を準備するようなイメージ。両方必要ですよね。
なので 「JestとRTLどっちを使うの?」 と言われたら 「両方使う」

describe, it, test

それぞれテストを読みやすく、整理された形で書くために使います。

describe

  • describeは特定のコンポーネントや関数に対する一連のテストをグループ化するために使用されます
// 例:LoginFormコンポーネントに関するテストをグループ化
describe('LoginForm', () => { xxx });

it

  • itは1つの特定の機能や挙動に関するテストを定義します。
describe('LoginForm', () => {
  // 例:LoginFormのメールアドレスとパスワードが正しくレンダリングされることを確かめるテストケース
  it('should render email and password fields correctly', () => { xxx });
});

test

  • testも1つの特定の機能や挙動に関するテストを定義します。基本的にはittestは同じものです。
  • 違いは、人の好みでよりテストケースがわかりやすい方を使います
describe('LoginForm', () => {
  // 例:LoginFormのメールアドレスとパスワードが正しくレンダリングされることを確かめるテストケース(test版)
  test('email and password fields render correctly', () => { xxx });
});

まとめるとdescribeでグループ化して、itまたはtestでテストケースを定義します。

getByXXX, queryByXXX, findByXXX

コンポーネントの要素(ボタンやテキストフィールドなど)を取得するのにこの3つを使います。
それぞれどう使うのかイメージしづらいので、説明します。

getByXXX

// 例:ボタンを取得する場合
const button = getByRole("button")
  • すぐに要素を取得する場合に使います。要素がなければエラーで、テストは失敗します。
  • なのでgetByXXXを使う場合は、「要素が存在するのは大前提」という場合です。(ログインフォームにログインボタンがあるのは当たり前、みたいな時)
  • 逆に、「存在しないことを確かめる」「少し時間が経てば現れることを確かめる」という場合には使いません

queryByXXX

// 例:ボタンを取得する場合
const button = queryByRole("button")
  • こちらもすぐに要素を取得する場合に使います。ただし、要素がなければ「ない」と返ってきます。
  • なのでqueryByXXXを使う場合は「要素が存在することを確かめる」場合と「要素がなくなったことを確かめる」という場合です。(通信が完了した後にローディングが消えたことを確認する、みたいな時)
  • 自分の使い分けとしては、要素を取得して何らかのイベントを起こしたい(クリックとか)場合はgetByXXXを使い、要素の存在を確かめたいときはqueryByXXXを使うことが多いです。

findByXXX

// 例:ボタンを取得する場合
const button = await findByRole("button")
  • これも要素を探しますが、上2つと違うのが要素がない場合です。findByXXXは要素がなければその要素が現れるまで待ちます(デフォルトの待ち時間は1000ミリ秒)
  • なので「要素が少し時間が経てば現れる」という場合に使います(ロード中のデータが表示されるまで待ちたい、みたいな時)
  • 非同期なのでawaitとセットが基本

waitFor

  • 非同期の表示を確かめる場合、使うことが多いのがwaitForです。これはfindByと使う部分が被っているのでどう違うのか解説します。
  • findByは上に書いた通り現れるまで待つ、というものです。
  • waitForは条件が満たされるまで待つ、というものです。
// 例:ログイン成功!というテキストを持つ要素が現れるまで待つ
await waitFor(() => {
  // この中の条件が全て満たされるまで待機する
  expect(screen.queryByText('ログイン成功!')).toBeInTheDocument();
});
  • どちらも似ていますが、waitForはより広範囲な条件に使えます。
  • 例えば、「ローディングが表示されなくなることを確かめる」や「コンポーネントの状態が特定の状態になるのを確かめる」など要素が現れる以外のこともテストできます。
// 例:ログイン成功!というテキストを持つ要素が現れるまで待つ。
// かつ、ローディング表示が消えることを確かめる
await waitFor(() => {
  expect(screen.queryByText('ログイン成功!')).toBeInTheDocument();
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
  • 僕は「確かめる要素が1つだけ」の場合にfindByを使います、それ以外は大体waitForを使っています。

fireEventとuserEvent

要素に対して入力したり、クリックしたりする時に使うのがfireEventuserEventです。
どっちも似たようなものですが、違いについて説明します。

fireEvent

// 例:ボタンをクリック
fireEvent.click(screen.getByRole('button'));
  • 特定のイベント(クリック、入力、フォーカスなど)を正確にテストする場合に向いている
  • 同期的。イベントが発火すると、即座に関連するイベントハンドラー(onClickなど)が呼び出される
  • 入力であれば、一度に全てのテキストが入力される

userEvent

// 例:ボタンをクリック
await userEvent.click(screen.getByRole('button'));
  • ユーザーの実際の挙動に近いテストをする場合に向いている(クリックであればクリック以外にもフォーカスなどクリックの前後に起こるイベントも発火する)
  • 非同期的。例えば入力フィールドへの入力であれば1文字ずつキーが入力されるのをシミュレートするので複数のイベントが発火される。そのため完了するまでに時間が必要、なのでawaitとセット

どっちを使ってもいいですが、今回のテストではuserEventを使います。

マッチャー

マッチャーとは?

  • マッチャーとはテスト中に値が期待通りかどうか確認するために使用する関数
  • expect関数とセットでexpect(実際の値).マッチャー(期待する値)の形で使う

よく使うマッチャー

  • toBeInTheDocument
    • 要素がDOMに存在するかをチェックする
    • 例:expect(element).toBeInTheDocument()
  • toHaveTextContent
    • 要素が特定のテキストを含むかチェックする
    • 例:expect(element).toHaveTextContent("text")
  • toBe
    • 値が等しいかチェックする
    • 例:expect(value).toBe(3)
  • toEqual
    • オブジェクトや配列の値が等しいかチェックする
    • 例:expect(object).toEqual({key: "value"})

問題

<LoginForm>の説明

  • ユーザーにメールアドレスとパスワードの入力を求めるフォームです。
  • 入力されたデータはzodスキーマによって検証されます
    • メールアドレスは有効な形式である必要があります。
    • パスワードは最低8文字以上である必要があります。
    • 不適切な入力がある状態で「ログイン」ボタンをクリックすると、エラーメッセージが表示されます
  • ユーザーが「ログイン」ボタンをクリックするとsignIn関数が呼び出され、入力されたメールアドレスとパスワードでログイン処理が開始されます。
  • ログイン処理中は、ボタンに「ローディング中...」と表示され、ボタンが無効化されます。
  • signIn関数からエラーが返された場合、エラーオブジェクトが返された場合、そのエラーメッセージが画面に表示されます。

サンプルコード

テスト対象のコードはこちらです。

LoginForm.tsx
import React, { useState } from "react";
import { z } from "zod";
import { SubmitHandler, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "./signin";

const userSchema = z.object({
  password: z.string().min(8, "パスワードは8文字以上必要です"),
  email: z.string().email("正しいメールアドレス形式ではありません"),
});

type InputType = z.infer<typeof userSchema>;

export const LoginForm = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [errMessage, setErrMessage] = useState("");

  const form = useForm<InputType>({
    resolver: zodResolver(userSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  const onSubmit: SubmitHandler<InputType> = async (data) => {
    setIsLoading(true);
    setErrMessage("");
    try {
      const result = await signIn("credentials", { ...data, callbackUrl: "/" });
      if (result?.error) {
        setErrMessage(result.error);
      }
    } catch (error) {
      const message = (() => {
        if (error instanceof Error) {
          return error.message;
        }
        return "ログインエラーです";
      })();
      setErrMessage(message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input
            id="email"
            type="text"
            placeholder="user@example.com"
            {...form.register("email", { required: true })}
          />
          {form.formState.errors.email && (
            <p>{form.formState.errors.email.message}</p>
          )}
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            type="password"
            placeholder="password"
            {...form.register("password", { required: true })}
          />
          {form.formState.errors.password && (
            <p>{form.formState.errors.password.message}</p>
          )}
        </div>
        <div>
          <button disabled={isLoading} type="submit">
            {isLoading ? "ローディング中..." : "ログイン"}
          </button>
          <p className="text-red-500">{errMessage}</p>
        </div>
      </form>
    </div>
  );
};
  • signIn関数はサーバーと通信をする、という想定です。
signin.ts
type SignInCredentials = {
  email: string;
  password: string;
};

type SignInOptions = {
  callbackUrl: string;
};

export const signIn = async (
  method: string,
  options: SignInOptions & SignInCredentials
): Promise<{ error?: string }> => {
  // メソッドが "credentials" で、オプションが有効な場合のみ処理を行う
  if (method === "credentials" && options) {
    // ダミーの認証ロジックを実装
    if (
      options.email === "user@example.com" &&
      options.password === "correctpassword"
    ) {
      // 認証成功
      return {};
    } else {
      // 認証失敗
      return { error: "認証に失敗しました" };
    }
  }
  // メソッドが "credentials" でない場合やオプションが無効な場合はエラーを返す
  return { error: "無効な認証方法またはオプションです" };
};

問題1

問題

次のテストを書こう!
メールアドレスとパスワードのフィールドが正しくレンダリングされることを確認する!

LoginForm.spec.tsx
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
import { signIn } from "./signin";

describe("LoginForm", () => {
  test("メールアドレスとパスワードのフィールドが正しくレンダリングされることを確認する", () => {
    // <LoginForm>をレンダリングする
    render(<LoginForm />);
    
    // ↓↓↓にテストコードを書く
  });
});
ヒント

要素の取得

// メールアドレスの入力フィールドを取得する
const emailField = screen.getByPlaceholderText("user@example.com");
// パスワードの入力フィールドを取得する
const passwordField = screen.getByPlaceholderText("password");

要素が表示されていることを確認するマッチャー

expect("要素").toBeInTheDocument();

答え

LoginForm.spec.tsx
describe("LoginForm", () => {
  test("メールアドレスとパスワードのフィールドが正しくレンダリングされることを確認する", () => {
    render(<LoginForm />);

    // メールアドレスの入力フィールドを取得する
    const emailField = screen.getByPlaceholderText("user@example.com");
    // パスワードの入力フィールドを取得する
    const passwordField = screen.getByPlaceholderText("password");

    // メールアドレスの入力フィールドが表示されていることを確認する
    expect(emailField).toBeInTheDocument();
    // パスワードの入力フィールドが表示されていることを確認する
    expect(passwordField).toBeInTheDocument();
  });
});

問題2

問題

フォームに入力データが正しく格納されることを確認する!

describe("LoginForm", () => {
  test("フォームオブジェクトに入力データが正しく格納されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
}
ヒント

取得した要素を入力フィールドとして扱う方法

const field = screen.getByPlaceholderText(
  "user@example.com"
) as HTMLInputElement;

入力フィールドに入力する方法

await userEvent.type(field, "test@example.com");

入力フィールドの値をテストする方法

expect(field.value).toBe("期待する値")

答え

  test("フォームオブジェクトに入力データが正しく格納されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
    const emailField = screen.getByPlaceholderText(
      "user@example.com"
    ) as HTMLInputElement;
    const passwordField = screen.getByPlaceholderText(
      "password"
    ) as HTMLInputElement;

    // 入力フィールドにテキストを入力する
    await userEvent.type(emailField, "test@example.com");
    await userEvent.type(passwordField, "testpassword");

    // 入力された内容が期待通りか確認する
    expect(emailField.value).toBe("test@example.com");
    expect(passwordField.value).toBe("testpassword");
  });

問題3

問題

フォームが送信された時に、signIn関数が正しく呼び出されることを確認する!
signIn関数はサーバーにデータを送っているという仮定で、テストではモックを使用してください。

describe("LoginForm", () => {
  test("フォームが送信された時に、signIn関数が正しく呼び出されることを確認する", async () => {
    render(<LoginForm />);
    
    // ↓↓↓にテストコードを書く
  });
}
ヒント

signIn関数のモック化

// jestのモック機能を使用して、./singinモジュール内の関数をモック化
jest.mock("./signin");

describe("LoginForm", () => {
  test("フォームが送信された時に、signIn関数が正しく呼び出されることを確認する", async () => {
    render(<LoginForm />);
    
    // ↓↓↓にテストコードを書く
    
    // jest.MockedFunction<typeof signIn>によって
    // signIn関数がモック関数であることを宣言し、
    // mockedSignIn.mock.callなどにアクセスできるようになる
    const mockedSignIn = signIn as jest.MockedFunction<typeof signIn>;
  });
}

signIn関数の引数の確認

// 第1引数
expect(mockedSignIn.mock.calls[0][0]).toXXX
// 第2引数
expect(mockedSignIn.mock.calls[0][1]).toXXX

答え

jest.mock("./signin");

describe("LoginForm", () => {
  test("フォームが送信された時に、signIn関数が正しく呼び出されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // jest.MockedFunction<typeof signIn>によって
    // signIn関数がモック関数であることを宣言し、
    // mockedSignIn.mock.callなどにアクセスできるようになる
    const mockedSignIn = signIn as jest.MockedFunction<typeof signIn>;

    // 値を直接入力
    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "test@example.com"
    );
    await userEvent.type(
      screen.getByPlaceholderText("password"),
      "testpassword"
    );

    // ユーザーがログインボタンをクリック
    userEvent.click(screen.getByRole("button"));

    // signIn関数が呼び出されたことをチェックする
    await waitFor(() => {
      expect(mockedSignIn).toHaveBeenCalled();
    });

    // signIn関数が期待される引数で呼び出されたことをチェックする
    expect(mockedSignIn.mock.calls[0][0]).toBe("credentials");
    expect(mockedSignIn.mock.calls[0][1]).toEqual({
      email: "test@example.com",
      password: "testpassword",
      callbackUrl: "/",
    });
  });
}

解説

モック化とは?

  • モック化とは、関数(例えばこのテストならサーバーにデータを送るsignIn関数)をテスト用の簡単な関数に置き換えることです。
  • これによりテスト中に実際のサーバーへデータを送信せずにテストを実行できます。
  • このテストでは、フォームが送信された時にsignIn関数が正しく呼び出されているかどうかをチェックしています。

問題4

問題

signIn関数でエラーが発生した場合に、エラ〜メッセージが表示されることを確認する
signIn関数がerrorオブジェクトを返した場合に、エラ〜メッセージが表示されることを確認する

  test("signIn関数でエラーが発生した場合に、エラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
  test("signIn関数がerrorオブジェクトを返した場合に、エラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
ヒント

signIn関数でエラーを発生させる

// signIn関数がエラーを返すようにモック化
(signIn as jest.Mock).mockRejectedValue(new Error("ネットワークエラー"));

signIn関数がerrorオブジェクトを返すようにする

// signIn関数がerrorオブジェクトを返すようにモック化
(signIn as jest.Mock).mockResolvedValue({ error: "認証エラー" });

答え

  test("signIn関数でエラーが発生した場合に、エラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // signIn関数がエラーを返すようにモック化
    (signIn as jest.Mock).mockRejectedValue(new Error("ネットワークエラー"));

    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "test@example.com"
    );
    await userEvent.type(
      screen.getByPlaceholderText("password"),
      "password123"
    );
    userEvent.click(screen.getByRole("button"));

    // エラーメッセージが表示されることを確認
    expect(await screen.findByText("ネットワークエラー")).toBeInTheDocument();

    // テスト後にモックをリセット
    (signIn as jest.Mock).mockReset();
  });
  test("signIn関数がerrorオブジェクトを返した場合に、エラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // signIn関数がerrorオブジェクトを返すようにモック化
    (signIn as jest.Mock).mockResolvedValue({ error: "認証エラー" });

    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "test@example.com"
    );
    await userEvent.type(
      screen.getByPlaceholderText("password"),
      "password123"
    );
    userEvent.click(screen.getByRole("button"));

    // エラーメッセージが表示されることを確認
    expect(await screen.findByText("認証エラー")).toBeInTheDocument();

    // テスト後にモックをリセット 
    (signIn as jest.Mock).mockReset();
  });

補足

どちらも最後にモックをリセットしています。しかしこれだと記述を忘れてリセットしないままにしてしまい、他のテストケースに影響が出る可能性があります。なので毎回テストケースでリセットするように修正しましょう

// jestのモック機能を使用して、./singinモジュール内の関数をモック化
jest.mock("./signin");

+ // 各テストケース終了時に呼び出される
+ afterEach(() => {
+   // モックをリセット
+   jest.resetAllMocks();
+ });

describe("LoginForm", () => { 
  // ~~~

問題5

問題

無効なメールアドレス入力時にエラーメッセージが表示されることを確認する!
パスワードが短すぎる場合にはエラ〜メッセージが表示されることを確認する!

  test("無効なメールアドレス入力時にエラーメッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
  test("パスワードが短すぎる場合にはエラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
ヒント

無効なメールアドレスの場合に期待されるエラーメッセージは「正しいメールアドレス形式ではありません」です。
パスワードが短すぎる場合に期待されるエラーメッセージは「パスワードは8文字以上必要です」です

答え

  test("無効なメールアドレス入力時にエラーメッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // 無効なメールアドレスを入力する
    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "invalid-email"
    );
    await userEvent.type(
      screen.getByPlaceholderText("password"),
      "password123"
    );

    // ログインボタンをクリック
    await userEvent.click(screen.getByRole("button"));

    expect(
      await screen.findByText("正しいメールアドレス形式ではありません")
    ).toBeInTheDocument();
  });
  test("パスワードが短すぎる場合にはエラ〜メッセージが表示されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "user@example.com"
    );
    // 短いパスワードを入力
    await userEvent.type(screen.getByPlaceholderText("password"), "short");

    // ログインボタンをクリック
    await userEvent.click(screen.getByRole("button"));

    expect(
      await screen.findByText("パスワードは8文字以上必要です")
    ).toBeInTheDocument();
  });

問題6

問題

ログインボタン押下時に、ローディングが表示されることとボタンが無効化されることを確認する!

  test("ログインボタン押下時に、ローディングが表示されることとボタンが無効化されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
  });
ヒント

signIn関数に遅延を設定する

// setTimeoutを使用して非同期処理をシミュレートする
(signIn as jest.Mock).mockImplementation(
  () => new Promise((resolve) => setTimeout(() => resolve({}), 1000))
);

答え

  test("ログインボタン押下時に、ローディングが表示されることとボタンが無効化されることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // setTimeoutを使用して非同期処理をシミュレートする
    (signIn as jest.Mock).mockImplementation(
      () => new Promise((resolve) => setTimeout(() => resolve({}), 1000))
    );

    await userEvent.type(
      screen.getByPlaceholderText("user@example.com"),
      "test@example.com"
    );
    await userEvent.type(
      screen.getByPlaceholderText("password"),
      "password123"
    );
    // ログインボタンをクリック
    userEvent.click(screen.getByRole("button"));

    await waitFor(() => {
      // ローディング中のテキストが表示されることを確認
      expect(screen.queryByText("ローディング中...")).toBeInTheDocument();
      // ローディング中にボタンが無効化されていることを確認
      expect(screen.getByRole("button")).toBeDisabled();
    });
  });

問題7

問題

signIn関数でエラーが発生した場合、ローディングの表示は解除され、ボタンが有効かすることを確認する

  test(
    "signIn関数でエラーが発生した場合、ローディングの表示は解除され、ボタンが有効かすることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く
    }
  );
ヒント

signIn関数に遅延を設定する

// signIn関数が1000ミリ秒後にエラーを返すように設定
(signIn as jest.Mock).mockImplementation(
  () =>
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("ログインエラー")), 1000)
    )
);

タイマーを置き換える

// Jestによってタイマーを置き換える
jest.useFakeTimers();

// setTimeoutが処理されて引き起こされる更新を適切に行うためにact()で囲む
act(() => {
  // 全てのタイマーを実行する
  jest.runAllTimers();
});

// タイマーを実際のものに戻す
jest.useRealTimers();

jest.useFakeTimers()を使用したときにuserEventを使う

const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

// 例:ログインボタンをクリック
user.click(screen.getByRole("button"));

答え

  test("signIn関数でエラーが発生した場合、ローディングの表示は解除され、ボタンが有効かすることを確認する", async () => {
    render(<LoginForm />);

    // ↓↓↓にテストコードを書く

    // userEventの設定
    const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

    // Jestによってタイマーを置き換える
    jest.useFakeTimers();

    // signIn関数が1000ミリ秒後にエラーを返すように設定
    (signIn as jest.Mock).mockImplementation(
      () =>
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error("ログインエラー")), 1000)
        )
    );

    await user.type(
      screen.getByPlaceholderText("user@example.com"),
      "test@example.com"
    );
    await user.type(screen.getByPlaceholderText("password"), "password123");
    // ログインボタンをクリック
    user.click(screen.getByRole("button"));

    await waitFor(() => {
      // ローディング中のテキストが表示されることを確認
      expect(screen.queryByText("ローディング中...")).toBeInTheDocument();
      // ローディング中にボタンが無効化されていることを確認
      expect(screen.getByRole("button")).toBeDisabled();
    });

    // 全てのタイマーを実行する
    act(() => {
      jest.runAllTimers();
    });

    await waitFor(() => {
      // ローディング中のテキストがないこととボタンが有効になっていることを確認
      expect(screen.queryByText("ローディング中...")).not.toBeInTheDocument();
      expect(screen.getByRole("button")).not.toBeDisabled();
    });

    // タイマーを実際のものに戻す
    jest.useRealTimers();
  });

解説

jest.useFakeTimers()を使用すると、なぜuserEventがタイムアウトするのか?

  • jest.useFakeTimers()を使用すると、jestはsetTimeoutなどを置き換える。この置き換えによってテスト中に時間の経過をシミュレートできるようになる。
  • しかしuserEventは内部でsetTimeoutを使用して、擬似的な遅延を実装している。そのためsetTimeoutを置き換えると、これらの遅延が進まなくなりタイムアウトすることがある
  • なのでuserEvent.setup({ advanceTimers: jest.advanceTimersByTime })を使用してjest.useFakeTimers()によって置き換えられたタイマーを各イベントに合わせて自動的に進むようにした

まとめ

最後に今まで解いた問題の一覧です。

  1. メールアドレスとパスワードのフィールドが正しくレンダリングされることを確認する!
  2. フォームに入力データが正しく格納されることを確認する!
  3. フォームが送信された時に、signIn関数が正しく呼び出されることを確認する!
  4. signIn関数でエラーが発生した場合に、エラ〜メッセージが表示されることを確認する
  5. signIn関数がerrorオブジェクトを返した場合に、エラ〜メッセージが表示されることを確認する
  6. 無効なメールアドレス入力時にエラーメッセージが表示されることを確認する!
  7. パスワードが短すぎる場合にはエラ〜メッセージが表示されることを確認する!
  8. ログインボタン押下時に、ローディングが表示されることとボタンが無効化されることを確認する!
  9. signIn関数でエラーが発生した場合、ローディングの表示は解除され、ボタンが有効かすることを確認する

ありがとうございました。

Discussion