User-Centric Testing: تست از دید کاربر، نه از دید پیادهسازی
در درس قبل با تست کامپوننت آشنا شدیم: رندر کردن یک کامپوننت و بررسی رفتار آن. اما همین سطح از تست را میتوان به دو شیوه کاملاً متفاوت نوشت؛ یکی شکننده و دردسرساز، دیگری پایدار و مفید. این درس به فلسفهای میپردازد که کتابخانههای مدرن تست React (مثل React Testing Library) بر پایه آن ساخته شدهاند: تست کردن از دید کاربر.
رویکرد اشتباه: تست جزئیات داخلی پیادهسازی
فرض کنید تستی بنویسیم که مستقیماً به State داخلی یک کامپوننت یا نام دقیق یک کلاس CSS وابسته باشد:
// شکننده: به جزئیات داخلی وابسته است
expect(wrapper.find("SearchBox").state("query")).toBe("");
expect(wrapper.find(".btn-primary-v2")).toHaveLength(1);مشکل این تستها این است که به چیزهایی وابستهاند که کاربر واقعی هرگز نمیبیند: نام یک State داخلی، یا نام دقیق یک کلاس CSS. اگر فردا این کامپوننت Refactor شود (مثلاً State با نام دیگری بازنویسی شود، یا کلاس CSS عوض شود)، بدون اینکه هیچ رفتار واقعی خراب شده باشد، این تست شکست میخورد؛ دقیقاً همان هزینهای که در درس اول این فصل به آن اشاره کردیم.
رویکرد درست: تست آنچه کاربر میبیند و انجام میدهد
بهجای پرسیدن «آیا State داخلی درست است؟»، سؤال درست این است: «اگر یک کاربر واقعی با این صفحه تعامل کند، چه چیزی میبیند و چه کاری میتواند انجام دهد؟»
// پایدار: از دید کاربر
render(<SearchBox />);
const input = screen.getByPlaceholderText("جستوجو...");
fireEvent.change(input, { target: { value: "کیبورد" } });
expect(input).toHaveValue("کیبورد");این تست، فقط میپرسد: آیا فیلدی با این Placeholder وجود دارد؟ آیا تایپ در آن، مقدارش را تغییر میدهد؟ این دقیقاً همان چیزی است که یک کاربر واقعی تجربه میکند، صرفنظر از اینکه داخل کامپوننت، State چه نامی دارد یا ساختار کد چگونه نوشته شده است.
چرا این رویکرد با Refactor سازگارتر است
اگر SearchBox را از useState به useReducer تغییر دهیم (موضوعی که در فصل نهم دیدیم)، یا نام یک متغیر داخلی را عوض کنیم، تست مبتنیبر کاربر، همچنان بدون تغییر، درست کار میکند؛ چون هیچکدام از این تغییرات، چیزی را که کاربر واقعی میبیند یا انجام میدهد، عوض نکردهاند. این دقیقاً همان شبکه ایمنی قابلاعتمادی است که در درس اول دنبالش بودیم.
ارتباط نزدیک با دسترسپذیری
نکته جالب این است که ابزارهایی مثل getByRole یا getByLabelText (که در درسهای بعدی با جزئیات بیشتری میبینیم)، دقیقاً همان اطلاعاتی را میخوانند که یک صفحهخوان هم استفاده میکند؛ یعنی نقشها و Labelهایی که در فصل هجدهم درباره دسترسپذیری یاد گرفتیم. نتیجه این است که نوشتن تست به سبک کاربر، معمولاً بهطور طبیعی، نوشتن کد قابلدسترستر را هم تشویق میکند؛ چون اگر یک عنصر از نظر معنایی درست نوشته نشده باشد (مثلاً یک div بهجای button)، پیدا کردن آن در تست هم سختتر میشود.
یک معیار ساده برای تشخیص تست خوب
پیش از نوشتن یک ادعا (Assertion) در تست، این سؤال را بپرسید: اگر این مقدار را از کاربر واقعی مخفی کنم (مثلاً با تغییر یک نام داخلی)، آیا او متوجه میشود؟ اگر پاسخ منفی است، آن مقدار، جزئیات پیادهسازی است و نباید مستقیم در تست بررسی شود.
مثال قابل اجرا
در این مثال، بررسی رفتار کاملاً از طریق چیزی انجام میشود که کاربر واقعی میبیند (متن دکمه)، نه یک مقدار داخلی پنهان.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>شمارش: {count}</button>;
}
function App() {
const [checkResult, setCheckResult] = useState(null);
function checkFromUserPerspective() {
const buttonText = document.querySelector("button").textContent;
setCheckResult(
buttonText.includes("شمارش")
? "متن قابلمشاهده برای کاربر درست است"
: "مشکلی در متن قابلمشاهده وجود دارد"
);
}
return (
<div>
<Counter />
<button onClick={checkFromUserPerspective}>بررسی از دید کاربر</button>
{checkResult && <p>{checkResult}</p>}
</div>
);
}جمعبندی
تستهایی که به جزئیات داخلی پیادهسازی (مثل نام State یا کلاس CSS) وابستهاند، با هر Refactor بیدلیل میشکنند. تستهای نوشتهشده از دید کاربر، فقط به چیزی که واقعاً دیده یا لمس میشود وابستهاند و در برابر تغییرات داخلی، پایدار باقی میمانند؛ این رویکرد، بهطور طبیعی با اصول دسترسپذیری هم همراستا است. در درس بعدی، به تست تعامل واقعی کاربر، مثل کلیک و تایپ، با جزئیات ابزارهای عملی میپردازیم.
