今回は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
要素のレンダリングが、事前に保存されているスナップショットと一致するか
以上になります。