
خلاصه:
اگه تستی مینویسی که با یه تغییر نام متغیر (state) میشکنه، یعنی داری اشتباه میزنی.
کاربر اصلاً براش مهم نیست کامپوننت تو با useState نوشته شده یا useReducer. اون فقط دکمه و متن رو میبینه.
قانون طلایی: تستهای تو باید دقیقاً مثل یک کاربر واقعی با نرمافزار رفتار کنن (کلیک کنن، تایپ کنن، ببینن)، نه مثل یک برنامه نویس که کد رو میخونه!
فرض کن یه کامپوننت خیلی ساده برای بخش FAQ سایتت نوشتی. وظیفهش مشخصه: یه سری سوال و جواب که با کلیک روی هر کدوم، باز و بسته میشن. برای مدیریت این باز و بسته شدن، یه state تعریف کردی به اسم isOpen. منطق سادهست: هر کلیک، مقدار isOpen رو برعکس میکنه.
بعد از پیادهسازی، مثل یه دولوپر حرفهای شروع میکنی به نوشتن تست. سعی میکنی مو به موی چیزی که نوشتی رو تست کنی:
"آیا isOpen اولش false هست؟"
"آیا بعد از کلیک isOpen میشه true؟"
همه چیز عالیه. ۱۰۰٪ Coverage. تستها رو اجرا میکنی و دیدن اون تیکهای سبز پشت سر هم، حس قدرت بهت میده. کامیت میکنی و به خودت که انقدر تمیز کد زدی و تست نوشتی افتخار میکنی!
یک ماه میگذره... توی یه تسک جدید، قراره یه فیچر اضافه کنی: توی متن یکی از آیتمها، یه "مودال" هم باز بشه که توضیحاتی درباره پرایسینگ داشته باشه. میری سراغ کد قدیمی که برای FAQ نوشته بودی. چشمت میخوره به isOpen. با خودت میگی: «این اسم الان دیگه مبهمه. اگه مودال هم باز بشه، دوتا isOpenداریم.»
یاد اون قانون معروف "Boy Scout Rule" میفتی:
You should always leave code a little cleaner than you found it!
«کد رو همیشه تمیزتر از چیزی که تحویل گرفتی، تحویل بده.»
پس تصمیم میگیری یه ریفکتور ریز انجام بدی:
اسم isOpen رو میکنی isItemOpen
یه استیت جدید میاری به اسم isModalOpen
کد تمیز شد، منطق همونه، همه چی کار میکنه. با خوشحالی git push میکنی. میری توی پنل CI/CD و منتظری سبز بشه تا دیپلوی کنی... اما یه لیست بلندبالا از ضربدرهای قرمز برای کامپوننت FAQ!
با خودت میگی: مگه منطق برنامه عوض شده؟ مگه باگ توی برنامه هست؟ کاربر هنوزم کلیک میکنه و آیتم باز میشه. پس چرا تست فیل شد؟
جوابی که باید داد اینه: تو داشتی "پیادهسازی" رو تست میکردی، نه "رفتار" رو.
تست تو انتظار داشت یه متغیر به اسم isOpen داشته باشی ولی وقتی تو اسم متغیر رو عوض کردی، تست گیج شد. این یعنی تست تو به جای اینکه محافظ کد باشه، شده مزاحم کد.
یعنی چی؟ بذار با یه مثال توضیح بدم.
بیا فرض کنیم تو یه جعبهی شفاف داری که تمام قطعات داخلیش پیداست. روی این جعبه یه کلید تعبیه شده و قراره با زدنش، لامپی که بالای جعبهست روشن بشه. داخل جعبه یه عالمه سیمکشی پیچیده هست: یه سیم آبی به مدار سمت چپ وصله، یه سیم قرمز به منبع تغذیه رفته و یه سیم سیاه هم به خود کلید وصل شده.
تو میای برای اطمینان از سالم بودن دستگاه، همچین تستی مینویسی:
test('circuit internals check', () => { const box = new ElectronicBox(); expect(box.internalWires.blue).toBeConnectedTo('circuit-A'); expect(box.internalWires.red).toBeConnectedTo('power-source'); expect(box.internalWires.black).toBeConnectedTo('switch'); expect(box.lightBulb).toBe('on'); });
همه چیز عالیه و تست ها پاس میشه. بعد از یه مدت، یه مهندس برق میاد و تصمیم میگیره سیستم داخلی رو بهینهتر کنه. اون تمام سیمهای رنگی رو با یه مدار جدید و مدرن جایگزین میکنه. دستگاه الان بهتر و سریعتر کار میکنه و لامپ هم با زدن کلید روشن میشه.
اما اتفاقی که میفته اینه: تستهای از کار افتادن! چرا؟ چون تو داشتی "پیادهسازی" رو تست میکردی، نه "رفتار" (روشن شدن لامپ).
داستان اینه که کاربر نهایی اصلاً براش مهم نیست اون تو سیم آبیه یا زرد، یا اصلاً سیمی وجود داره یا نه؛ اون فقط میخواد وقتی کلید رو زد، لامپ روشن بشه. در واقع پیادهسازی تو مشکلی نداره، فقط تستت داره False Negative میده!
در واقع تست درست برای این رفتار باید به شکل زیر باشه:
test('light turns on when switch is pressed', () => { const box = new ElectronicBox(); user.press(box.switch); expect(box.lightBulb).toBe('on'); });
تو این روش، مهندس برق میتونه کل محتویات جعبه رو برداره و جاش یه همستر بذاره که با دویدن روی یه گردونه برق تولید میکنه! تا زمانی که با زدن کلید لامپ روشن بشه، تست تو سبز میمونه. این یعنی تست در خدمت کد توعه، نه مزاحم اون.
در واقع با این مدل تستنویسی، تو کاری که انجام میدی اینه که داری رفتار یه کاربر رو تست میکنی. حتی اگه تمام کدی که با React زدی رو بریزی دور و اون رو به Vue تبدیل کنی، باز هم تستهای تو دارن کار میکنن چون بر مبنای "رفتار کاربر" نوشته شدن.
اگه تستت شکست و مجبوری بری تو کد پیادهسازی (implementation) نگاه کنی که بفهمی چرا شکست، یعنی تستت اشتباه نوشته شده!
احتمالاً شنیدید که میگن باید Unit Test زیاد بنویسید، Integration کمتر و E2E خیلی کمتر (هرم تست).
اما وقتی با رویکرد "تست رفتار" جلو میریم، معادلات عوض میشه. ما دیگه هرم تست رو میذاریم کنار و میریم سراغ جام تست (Testing Trophy; Kent C. Dodds). تو این مفهوم مدرن، بخش Integration بزرگترین و مهمترین سهم رو تو استراتژی تستنویسی داره. به جای اینکه خودمون رو توی باتلاق هزاران Unit Test ریز غرق کنیم یا درگیر تستهای E2E کند بشیم، تمرکز اصلی رو میذاریم روی تستهایی که تعامل بخشهای مختلف رو چک میکنن.
حالا هدف از این همه توضیحات چیه؟ اعتماد به نفس! تستنویسی نباید باعث بشه از تغییر دادن کد بترسی. برعکس، تستها باید بهت این قدرت رو بدن که کد رو زیر و رو کنی، ریفکتور کنی و فیچر جدید بزنی، در حالی که مطمئنی "تجربه کاربر" هیچ تغییری نکرده. این یعنی تست در خدمت تو، نه تو در خدمت تست.
حالا که ذهنیتمون رو از "تست کد" به "تست رفتار" تغییر دادیم، توی قسمتهای بعدی دست به کد میشیم و میریم سراغ ابزارهای تستنویسی تا یاد بگیریم چطور توی دنیای واقعیِ React این کار رو انجام بدیم.
امیدوارم این مقاله کمک کرده باشه که تستنویسی براتون سادهتر و شیرینتر بشه.