0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Testing Libraryで実際にinputコンポーネントのテストをしてみた

Last updated at Posted at 2025-10-15

今回はreactでTesting Libraryを使ってinputコンポーネントのテストをしてみました
下記のサイトを参考にしました
https://testing-library.com/docs/queries/about

まず使うinputコンポーネントは下記のとおりです

import React from "react";
import type { InputType } from "@/types/ui";

type Props = {
  id: string;
  label?: string;
  type: InputType;
  value: string;
  disabled?: boolean;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  error?: string;
};

const Input = ({
  id,
  label,
  type,
  value,
  disabled,
  onChange,
  error,
}: Props) => {
  return (
    <div className="flex flex-col space-y-1">
      {label && (
        <label htmlFor={id} className="font-bold text-gray-700">
          {label}
        </label>
      )}
      <input
        id={id}
        type={type}
        value={value}
        onChange={onChange}
        disabled={disabled}
        className={
          "border-b focus:outline-none focus:border-b-2 focus:border-indigo-500 disabled:bg-gray-100 disabled:text-gray-400 "
        }
      />
      {error && <p className="text-red-500 text-sm ml-1">{error}</p>}
      {disabled && <input type="hidden" value={value?.toString()} />}
    </div>
  );
};

export default Input;

簡単に説明すると、propsで受け取る引数は、それぞれ以下の通りです

id

inputタグのid、labelのforに紐づいています。

label

ラベルタグの中身の値になります。

type

"text","number","email","tel","password","url", "date"からtypeを選ぶようになっています(別のtypeファイルを各項目を設けています)

value

inputタグのvalueに当たります。下記のonChangeとuseStateで値は変わっていきます

onChange

inputタグのonChangeに当たり、基本的に親コンポーネントがuseStateを使います

disabled

boolで、inputタグのdisabledを制御します。
trueの時もデータはpostしたいので隠しinputタグを持たせています

error

エラー発生時に、メッセージを出すpタグです

今回はこのコンポーネントに対してテストを行っていきます。

実際のテストコードはこんな感じです

import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Input from "./Input";

//props毎にテスト

describe("Input component", () => {
  it("labelは表示されるか", () => {
    render(
      <Input
        id="labelTest"
        type="text"
        value=""
        onChange={() => {}}
        label="ラベル"
      />
    );
    expect(screen.getByText("ラベル")).toBeInTheDocument();
    expect(screen.getByLabelText("ラベル")).toBeInTheDocument();
  });

  it("labelは表示されないか", () => {
    render(<Input id="labelTest" type="text" value="" onChange={() => {}} />);
    expect(
      screen.queryByText("ラベルは存在しないのでnullを返します")
    ).not.toBeInTheDocument();
  });

  it("初期値の制御はできるか", () => {
    render(
      <Input
        id="defaultValueTest"
        type="text"
        value="HelloWorld"
        onChange={() => {}}
      />
    );
    expect(screen.getByDisplayValue("HelloWorld")).toBeInTheDocument();
  });

  it("onChangeイベントが発火するか", () => {
    const handleChange = vi.fn();
    render(<Input id="input" type="text" value="" onChange={handleChange} />);
    const input = screen.getByRole("textbox");
    fireEvent.change(input, { target: { value: "テスト" } });
    expect(handleChange).toHaveBeenCalledTimes(1);
  });

  it("errorは表示されるか", () => {
    render(
      <Input
        id="errorTest"
        type="text"
        value=""
        onChange={() => {}}
        error="エラーがあります"
      />
    );
    expect(screen.getByText("エラーがあります")).toBeInTheDocument();
  });

  it("typeが正しく反映されるか", () => {
    render(
      <Input id="typeTest" type="number" value="123" onChange={() => {}} />
    );
    const input = screen.getByDisplayValue("123");
    expect(input).toHaveAttribute("type", "number");
  });

  it("disabledは反映されるか", () => {
    render(
      <Input
        id="disabledTest"
        type="text"
        value="非活性テスト"
        onChange={() => {}}
        disabled
      />
    );
    const input = screen.getByRole("textbox");
    expect(input).toBeDisabled();
  });

  it("disabled時にpostできるか", () => {
    render(
      <Input
        id="disabledTest"
        type="text"
        value="非活性テスト"
        onChange={() => {}}
        disabled
      />
    );
    const hiddenInput = screen.getByRole("textbox", { hidden: true });
    expect(hiddenInput).toBeInTheDocument();
  });

  it("スナップショットテスト", () => {
    const { asFragment } = render(
      <Input id="snapshot" type="text" value="abc" onChange={() => {}} />
    );
    expect(asFragment()).toMatchSnapshot();
  });
});

それぞれのprops毎にテスト観点を作り、テストを行いました。
テストメソッドをまとめるとこんな感じです

getByText

指定されたテキストに一致する要素は存在するか

getByLabelText

指定されたラベルのテキストに紐づいたinputが存在するか

queryByText

指定されたテキストに一致する要素は存在するか、なおない場合はnullを返す
→存在しないことを確認したい

getByRole

指定されたロールを持つ要素を検索してくれる(input="text"やオプションでtype="hidden"とか)

getByDisplayValue

現在表示されている値(value)を基準に要素を取得する

toHaveAttribute

指定された要素が値(value)を持っているか

※vi.fn()

vitestのモック関数

※fireEvent.change

fireEvent:change イベントを手動で発火

toHaveBeenCalledTimes(n)

特定の関数がn回呼び出されたか

toMatchSnapshot

要素のレンダリングが、事前に保存されているスナップショットと一致するか

以上になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?