Testing Libraryでフォームの値を編集するときのテストを書くときに気をつけること

テキスト入力フォームの特徴は「入力値をユーザーが好きに編集できる点」である。テストケースもフォームの値を編集した後UIが変わることを確かめたい場合が多い(例:入力値に応じてバリデーションチェックを行う)。フォーム入力はDOMイベントになるためuserEventで再現すれば良い。

イベント名はtype1を利用する。入力内容は二番目の引数に指定する。実際のテストケースは次のようになる。

describe("フォーム入力値を編集したときのテスト", () => {
  it("苗字のフォームを編集すると入力したときの値でテキストが表示される", async () => {
    const event = userEvent.setup();
    const content = render(<StateForm />);

    // 苗字のフォームを編集する
    await event.type(content.getByRole("textbox", { name: "苗字" }), "田中");

    // フォームの値が変更されたことを確認
    expect(content.getByRole("textbox", { name: "苗字" })).toHaveValue("田中");

    // 苗字のフォームを編集したときの値でテキストが表示される
    expect(
      content.getByText("こんにちは、田中 太郎さん!")
    ).toBeInTheDocument();
  });
});

テキスト入力フォームに元々valueが入力されている場合、typeだけでは意図取りテストできない。次のように「初期値 + typeの入力値」がフォームに入力された状態になってしまう。typeは元々入力されていた値を削除する機能は存在しないからである。

 FAIL  src/component/Form/NameForm.test.tsx
  初期表示に関するテスト
    ✓ 苗字のフォームには山田と入力されている (33 ms)
    ✓ 名前のフォームには太郎と入力されている (7 ms)
  フォーム入力値を編集したときのテスト
    ✕ 苗字のフォームを編集すると入力したときの値でテキストが表示される (49 ms)

  ● フォーム入力値を編集したときのテスト › 苗字のフォームを編集すると入力したときの値でテキストが表示される

    expect(element).toHaveValue(田中)

    Expected the element to have value:
      田中
    Received:
      山田田中

      25 |
      26 |     // フォームの値が変更されたことを確認
    > 27 |     expect(content.getByRole("textbox", { name: "苗字" })).toHaveValue("田中");
         |                                                          ^
      28 |
      29 |     // 苗字のフォームを編集したときの値でテキストが表示される
      30 |     expect(

      at Object.toHaveValue (src/component/Form/NameForm.test.tsx:27:58)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        0.986 s, estimated 1 s
Ran all test suites matching /src\/component\/Form\/NameForm.test.tsx/i.

実際のフォーム編集を思い浮かべていただきたいが、フォームの値を全く違うものに変えたい場合はBackspaceキーなどで空にしてから編集することが多い。userEventはユーザー操作を再現するコンセプトでできているため「フォームを空にしてから」という操作を再現してやる必要がある。

Backspaceキーの動きを再現しても良いが、デフォルト入力値の変更のたびにキーの回数を変更しなければならない。 するとテストのメンテナンスが大変なので、一括で初期化するclear2を使うと良い。 clearAPIはvalueの値をリセットする機能を持っている。

describe("フォーム入力値を編集したときのテスト", () => {
  it("苗字のフォームを編集すると入力したときの値でテキストが表示される", async () => {
    const event = userEvent.setup();
    const content = render(<StateForm />);

    // フォームを初期化する
    await event.clear(content.getByRole("textbox", { name: "苗字" }));

    // 苗字のフォームを編集する
    await event.type(content.getByRole("textbox", { name: "苗字" }), "田中");

    // フォームの値が変更されたことを確認
    expect(content.getByRole("textbox", { name: "苗字" })).toHaveValue("田中");

    // 苗字のフォームを編集したときの値でテキストが表示される
    expect(
      content.getByText("こんにちは、田中 太郎さん!")
    ).toBeInTheDocument();
  });
});

このように、入力値があるときは連続して入れるのか・初期化をする方が良いのかを考えながらテストする。TODOを最初に立てておくと、何を再現するのか迷わずに済む。