تست کامپوننت‌ها و رفتار کاربر: جمع‌بندی عملی فصل

در طول این فصل، چرایی تست، انواع آن، فلسفه تست از دید کاربر، تعامل، Mocking و تست UIهای Async را جداگانه دیدیم. در این درس آخر فصل، همه این‌ها را در تست یک کامپوننت کامل‌تر و واقعی‌تر، یک فرم ثبت‌نام ساده، کنار هم می‌گذاریم.

کامپوننت موردنظر

jsx
function SignupForm({ onSubmit }) {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    if (!email.includes("@")) {
      setError("ایمیل معتبر نیست");
      return;
    }
    setError("");
    onSubmit(email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">ایمیل</label>
      <input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      {error && <p>{error}</p>}
      <button type="submit">ثبت‌نام</button>
    </form>
  );
}

این کامپوننت، ترکیبی از همان چیزهایی است که در فصل چهارم (فرم و Validation) ساختیم.

قدم اول: تست رندر اولیه

javascript
test("فرم در حالت اولیه، بدون پیام خطا نمایش داده می‌شود", () => {
  render(<SignupForm onSubmit={jest.fn()} />);

  expect(screen.getByRole("textbox", { name: "ایمیل" })).toHaveValue("");
  expect(screen.queryByText("ایمیل معتبر نیست")).not.toBeInTheDocument();
});

jest.fn() یک تابع Mock ساده می‌سازد که فقط فراخوانی‌هایش را ثبت می‌کند؛ چون در این تست، به خود onSubmit کاری نداریم، فقط لازم است کامپوننت یک تابع دریافت کند تا خطا ندهد. queryByText (برخلاف getByText)، اگر عنصر پیدا نشود، به‌جای خطا، null برمی‌گرداند؛ به همین دلیل برای بررسی «این متن نباید وجود داشته باشد» مناسب‌تر است.

قدم دوم: تست مسیر نامعتبر

javascript
test("ارسال فرم با ایمیل نامعتبر، پیام خطا نشان می‌دهد و onSubmit فراخوانی نمی‌شود", async () => {
  const handleSubmit = jest.fn();
  render(<SignupForm onSubmit={handleSubmit} />);

  await userEvent.type(screen.getByRole("textbox", { name: "ایمیل" }), "نامعتبر");
  await userEvent.click(screen.getByRole("button", { name: "ثبت‌نام" }));

  expect(screen.getByText("ایمیل معتبر نیست")).toBeInTheDocument();
  expect(handleSubmit).not.toHaveBeenCalled();
});

اینجا بررسی می‌کنیم که نه‌تنها پیام خطا ظاهر شده، بلکه تابع onSubmit هم اصلاً فراخوانی نشده است؛ این نوع بررسی روی یک تابع Mock، به ما اطمینان می‌دهد منطق Validation واقعاً از ادامه جریان جلوگیری کرده است.

قدم سوم: تست مسیر معتبر

javascript
test("ارسال فرم با ایمیل معتبر، onSubmit را با همان مقدار فراخوانی می‌کند", async () => {
  const handleSubmit = jest.fn();
  render(<SignupForm onSubmit={handleSubmit} />);

  await userEvent.type(screen.getByRole("textbox", { name: "ایمیل" }), "test@example.com");
  await userEvent.click(screen.getByRole("button", { name: "ثبت‌نام" }));

  expect(handleSubmit).toHaveBeenCalledWith("test@example.com");
});

چرا این سه تست با هم، پوشش معناداری می‌دهند

این سه تست، دقیقاً همان سه مسیر منطقی مهم کامپوننت را پوشش می‌دهند: حالت اولیه، رد شدن مقدار نامعتبر، و پذیرفته شدن مقدار معتبر؛ بدون این‌که هیچ‌کدام به جزئیات داخلی (مثل نام State email یا error) وابسته باشند. اگر فردا این فرم با useReducer بازنویسی شود، هر سه تست، بدون تغییر، همچنان درست کار می‌کنند.

یک اصل پایانی برای کل فصل

تست خوب، داستانی از رفتار کاربر تعریف می‌کند: «وقتی کاربر این کار را انجام دهد، باید این نتیجه را ببیند». هر تستی که این داستان را دنبال نکند و به‌جای آن به جزئیات فنی بپردازد، به مرور زمان تبدیل به بار اضافی می‌شود، نه ابزار اطمینان.

مثال قابل اجرا

فرم ثبت‌نام آماده برای سناریوهای تست این درس
import { useState } from "react";

function SignupForm({ onSubmit }) {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    if (!email.includes("@")) {
      setError("ایمیل معتبر نیست");
      return;
    }
    setError("");
    onSubmit(email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">ایمیل</label>
      <input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      {error && <p>{error}</p>}
      <button type="submit">ثبت‌نام</button>
    </form>
  );
}

function App() {
  const [submittedEmail, setSubmittedEmail] = useState(null);
  return (
    <div>
      <SignupForm onSubmit={setSubmittedEmail} />
      {submittedEmail && <p>ثبت شد: {submittedEmail}</p>}
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

تست کامل یک کامپوننت واقعی، معمولاً چند سناریو را پوشش می‌دهد: حالت اولیه، مسیرهای نامعتبر و معتبر، با بررسی هم‌زمان آنچه روی صفحه دیده می‌شود و اینکه آیا توابع بیرونی مثل onSubmit، با مقدار درست، فراخوانی شده‌اند یا نه. این رویکرد، نتیجه ترکیب تمام اصولی است که در طول فصل دیدیم: تست از دید کاربر، تعامل واقعی، و استفاده هدفمند از Mock. با این درس، فصل نوزدهم به پایان می‌رسد و در ادامه به آزمون فصل ۱۹ می‌رویم.