انواع تست در برنامه‌های React

در درس قبل دیدیم چرا اصلاً تست می‌نویسیم. اما «تست» یک مفهوم یکدست نیست؛ تست‌های مختلف، در سطوح متفاوتی از برنامه را بررسی می‌کنند و هزینه و سرعت اجرای متفاوتی دارند. این درس، سه سطح اصلی تست را معرفی می‌کند و نشان می‌دهد هرکدام چه سؤالی را پاسخ می‌دهند.

سطح اول: تست واحد (Unit Test)

تست واحد، کوچک‌ترین واحد ممکن از منطق را، جدا از بقیه برنامه، بررسی می‌کند؛ مثلاً یک تابع خالص یا یک Custom Hook ساده که در فصل دهم ساختیم.

javascript
function formatPrice(amount) {
  return amount.toLocaleString("fa-IR") + " تومان";
}

// تست واحد همین تابع
test("formatPrice یک عدد را به‌درستی فرمت می‌کند", () => {
  expect(formatPrice(1000)).toBe("۱٬۰۰۰ تومان");
});

این نوع تست، سریع‌ترین نوع اجراست، چون هیچ کامپوننتی رندر نمی‌شود؛ فقط یک تابع فراخوانی و خروجی آن بررسی می‌شود.

سطح دوم: تست کامپوننت (Component Test)

این سطح، یک کامپوننت را به‌تنهایی رندر می‌کند و رفتار آن را، از دید کاربر، بررسی می‌کند؛ مثلاً آیا با کلیک روی یک دکمه، متن صفحه عوض می‌شود؟

jsx
test("کلیک روی دکمه، شمارنده را افزایش می‌دهد", () => {
  render(<Counter />);
  const button = screen.getByRole("button");
  fireEvent.click(button);
  expect(button).toHaveTextContent("1");
});

این نوع تست، موضوع اصلی باقی این فصل است و با جزئیات کامل در درس‌های بعدی بررسی می‌شود؛ از جمله ابزارهایی مثل render، screen و fireEvent که در این مثال دیده می‌شوند.

سطح سوم: تست سرتاسری (End-to-End، یا E2E)

تست E2E، یک مرورگر واقعی را کنترل می‌کند و کل برنامه را، دقیقاً مثل یک کاربر واقعی، از ابتدا تا انتهای یک جریان کامل (مثلاً ورود، افزودن محصول به سبد، و پرداخت) طی می‌کند. این سطح، نزدیک‌ترین تست به تجربه واقعی کاربر است، اما کندترین و پرهزینه‌ترین نوع اجراست.

چرا هر سه سطح لازم است، نه فقط یکی

یک تمثیل رایج برای این موضوع، هرم تست (Testing Pyramid) است: تعداد زیادی تست واحد سریع در پایه، تعداد متوسطی تست کامپوننت در میانه، و تعداد کمی تست E2E در رأس. این ساختار، به این دلیل است که تست‌های سریع‌تر، بازخورد زودتری می‌دهند و هزینه کمتری برای نگه‌داری دارند؛ اما فقط تست‌های E2E می‌توانند مطمئن شوند همه بخش‌های برنامه، واقعاً با هم به‌درستی کار می‌کنند.

کدام سطح را برای چه چیزی انتخاب کنیم

یک تابع خالص یا منطق پیچیده بدون وابستگی به DOM (مثل یک Reducer که در فصل نهم دیدیم)، کاندیدای خوبی برای تست واحد است. رفتار یک کامپوننت مجزا (مثل یک فرم یا یک Dropdown که در فصل قبل ساختیم)، معمولاً با تست کامپوننت بررسی می‌شود. یک جریان حیاتی و کامل کسب‌وکار (مثل فرایند کامل پرداخت)، ارزش نوشتن یک تست E2E را دارد، اما نیازی نیست هر جزئیات کوچک در این سطح تکرار شود.

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

در این مثال، همان ایده تست واحد (برای یک تابع ساده) و تست کامپوننت (برای رفتار یک دکمه)، بدون فریمورک واقعی تست، به‌صورت دستی شبیه‌سازی شده‌اند.

شبیه‌سازی دستی یک تست واحد و یک تست کامپوننت
import { useState } from "react";

function formatPrice(amount) {
  return amount.toLocaleString("fa-IR") + " تومان";
}

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

function App() {
  const unitTestPassed = formatPrice(1000).includes("تومان");

  return (
    <div>
      <p>نتیجه تست واحد formatPrice: {unitTestPassed ? "موفق" : "ناموفق"}</p>
      <p>کامپوننت زیر را دستی مثل یک تست کامپوننت امتحان کنید:</p>
      <Counter />
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

تست واحد، منطق جدا و کوچک را سریع بررسی می‌کند؛ تست کامپوننت، رفتار یک کامپوننت را از دید کاربر، بدون مرورگر واقعی، می‌سنجد؛ و تست E2E، کل برنامه را در یک مرورگر واقعی، طی یک جریان کامل، آزمایش می‌کند. ترکیب درست این سه سطح، بیشترین اطمینان را با کمترین هزینه ممکن می‌دهد. در درس بعدی، به‌طور مشخص به فلسفه User-Centric Testing، که پایه نوشتن تست‌های خوب کامپوننت است، می‌پردازیم.