چرا تست: ارزش واقعی نوشتن تست برای یک برنامه React

تا اینجا در این دوره، درستی هر مثال را با اجرای دستی و نگاه کردن به نتیجه بررسی کردیم. اما در یک پروژه واقعی، با رشد تعداد کامپوننت‌ها و تعامل بین آن‌ها، بررسی دستی هر بار، هم کند است و هم مستعد نادیده گرفتن یک خطای ظریف. این فصل به تست در React می‌پردازد؛ و این درس اول، به این سؤال پاسخ می‌دهد که اصلاً چرا باید زمان صرف نوشتن تست کرد.

مشکل: اعتماد بر پایه حافظه و بررسی دستی

فرض کنید یک کامپوننت فرم را تغییر می‌دهید تا یک قابلیت جدید اضافه شود. از کجا مطمئن می‌شوید که این تغییر، رفتار قدیمی (مثل اعتبارسنجی یا مدیریت Submit که در فصل چهارم دیدیم) را خراب نکرده است؟ بدون تست، تنها راه، باز کردن مرورگر و امتحان دستی همه حالت‌های ممکن است؛ کاری که با رشد پروژه، عملاً غیرممکن می‌شود.

تست به‌عنوان یک شبکه ایمنی برای تغییر

یک مجموعه تست خوب، دقیقاً همان کاری را انجام می‌دهد که بررسی دستی انجام می‌داد، اما خودکار و در چند ثانیه: اجرای رفتار کامپوننت و بررسی اینکه نتیجه هنوز همان چیزی است که انتظار می‌رود. اگر یک تغییر، رفتار قبلی را خراب کند، تست‌ها فوراً این موضوع را نشان می‌دهند؛ پیش از اینکه کاربر واقعی با آن مواجه شود.

تست، مستندی زنده از رفتار موردانتظار

یک فایده کمتر شناخته‌شده تست، این است که خودش نوعی مستندات است. وقتی کسی (حتی خود شما، چند ماه بعد) به یک فایل تست نگاه می‌کند، می‌تواند دقیقاً ببیند این کامپوننت قرار است در چه شرایطی، چه رفتاری داشته باشد؛ بدون نیاز به خواندن کل پیاده‌سازی داخلی آن.

تست، در برابر ترس از Refactor

در فصل سیزدهم دیدیم که گاهی لازم است ساختار کامپوننت‌ها برای بهبود عملکرد یا خوانایی تغییر کند (Refactor). بدون تست، هر Refactor یک ریسک است: شاید رفتار بیرونی را ناخواسته عوض کند. با وجود تست‌هایی که همان رفتار بیرونی را بررسی می‌کنند، می‌توان ساختار داخلی را با اطمینان بیشتری تغییر داد، چون تست‌ها فوراً خبر می‌دهند اگر چیزی واقعاً خراب شده باشد.

تست، رایگان نیست

نوشتن و نگه‌داری تست، خودش زمان می‌برد؛ و تست‌های نوشته‌شده بد (مثلاً وابسته به جزئیات داخلی پیاده‌سازی، نه رفتار واقعی) می‌توانند حتی سرعت توسعه را کم کنند، چون با هر تغییر کوچک و بی‌ضرر کد، بی‌دلیل شکست می‌خورند. در درس‌های بعدی این فصل، به‌خصوص وقتی به User-Centric Testing می‌رسیم، می‌بینیم چگونه از این دام دوری کنیم.

یک مثال ملموس: چرا نبود تست، هزینه دارد

jsx
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

فرض کنید بعداً کسی این کامپوننت را تغییر دهد و به‌اشتباه setCount(count + 1) را به setCount(count - 1) تبدیل کند. اگر هیچ تستی وجود نداشته باشد، این باگ ممکن است تا رسیدن به محیط واقعی و شکایت کاربران، کشف نشود. یک تست ساده که بررسی می‌کند «با کلیک، عدد افزایش پیدا می‌کند»، این اشتباه را همان لحظه، پیش از انتشار، نشان می‌دهد.

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

در این مثال، چون اجرای واقعی یک فریمورک تست در این محیط ممکن نیست، یک تابع بررسی ساده نوشته شده تا همان ایده اصلی «اجرای رفتار و مقایسه نتیجه با انتظار» دیده شود.

شبیه‌سازی ساده ایده یک تست
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button data-testid="counter" onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

function App() {
  const [result, setResult] = useState(null);

  function runCheck() {
    // شبیه‌سازی: آیا رفتار موردانتظار برقرار است؟
    const expected = "افزایش با کلیک، رفتار درستی است";
    setResult(expected);
  }

  return (
    <div>
      <Counter />
      <button onClick={runCheck}>اجرای بررسی رفتار</button>
      {result && <p>{result}</p>}
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

تست نوشتن، جایگزین بررسی دستی می‌شود و در برابر خراب شدن ناخواسته رفتار قدیمی، هنگام افزودن قابلیت جدید یا Refactor، یک شبکه ایمنی خودکار می‌سازد؛ در کنار این، خودش مستنداتی زنده از رفتار موردانتظار هر کامپوننت است. این مزیت‌ها، در ازای زمان نوشتن و نگه‌داری تست به دست می‌آیند، و تست نوشته‌شده بد می‌تواند این هزینه را حتی از فایده‌اش بیشتر کند. در درس بعدی، انواع مختلف تست در یک برنامه React را بررسی می‌کنیم.