اگر امروز به عاملهای هوش مصنوعی برای نوشتن تستهای واحد (Unit Test) اعتماد میکنید، احتمالاً با یک دروغ سبز رنگ مواجه هستید. بسیاری از این تستها «توخالی» (Hollow Tests) هستند؛ یعنی ادعاهایی که حتی پس از تخریب عمدی کد تولیدی (Production Code)، همچنان سبز میمانند و پاس میشوند. این پدیده که در یک راهنمای فنی مفصل در تاریخ ۳۰ سپتامبر ۲۰۲۶ فاش شد، نشان میدهد که وقتی یک مجموعه تست توسط یک عامل AI نوشته میشود، پاس شدن آن اغلب یک توهم است.
این شکست به این دلیل رخ میدهد که عاملها معمولاً همان پیشفرضهای غلطی را که در پیادهسازی کد به کار بردهاند، در نوشتن تست هم تکرار میکنند. در واقع، مدل هم کد را اشتباه مینویسد و هم تستی مینویسد که همان اشتباه را تایید میکند. در حالی که صنعت برای حل این مشکل به سمت توسعهمدیریتشده توسط تست (TDD) حرکت کرده است، چرخه «قرمز-سبز-بازسازی» (red-green-refactor) به دلیل اتوماسیون توسط عاملها در حال تضعیف است.
تصور کنید یک بازرس ایمنی، خودش قوانین ایمنی ساختمان را بنویسد و سپس با همان قوانین ناقص، ساختمان را بازرسی کند؛ اگر قوانین غلط باشند، ساختمان فرو میریزد اما بازرس به آن مدال افتخار میدهد. این دقیقاً وضعیتی است که در TDD با حضور هوش مصنوعی رخ میدهد. همانطور که در تحلیلهای قبلی ما دربارهی توهمات مدلهای زبانی اشاره کردیم، این مدلها در تاییدِ اشتباهات خود بسیار متقاعدکننده عمل میکنند. این چالش با شکاف میان صحتِ پیادهسازی و صحتِ سیستمی در عاملهای کدنویس که پیشتر بررسی کردیم، همسو است و نشان میدهد که صرفاً نوشتن کد درست، به معنای درست کار کردن کل سیستم نیست.
تکامل TDD در عصر عاملها
به نقل از گزارش dev.to، رویکرد تستنویسی با هوش مصنوعی از سه مرحله متمایز عبور کرده است که هر کدام مشکلی خاص در جریان کاری عاملها را حل میکنند:
- تستهای انسانمحور: در ابتدا توصیه میشد تستها در دست انسان بماند. منطق این بود که اگر یک مدل هم کد و هم تست را بنویسد، یک پیشفرض واحد در هر دو طرف قرار میگیرد. در این مدل، انسانها تستها را مینوشتند و AI کد را پیاده میکرد. این روش هنوز زمانی که «مشخصات» (Specification) مهمتر از «سرعت» باشد، استاندارد طلایی است.
- چرخههای دستور-محور: با افزایش سرعت تولید تست توسط عاملها (که میتوانند در زمان نوشتن یک تست توسط انسان، بیست تست تولید کنند)، تیمها به استفاده از مکانیسمهای دستوری برای اجبار به رعایت چرخه روی آوردند. این شامل فایلهایی مانند
AGENTS.mdدر ریشه پروژه،.bob/rulesبرای IBM Bob،CLAUDE.mdبرای Claude Code و.cursor/rulesبرای Cursor است. این فایلها کنوانسیونهای پروژه را حمل میکنند: یک رفتار در هر چرخه، وجود یک دروازبان انسانی قبل از پیادهسازی و گاهی ثبت تست قرمز در تاریخچه git. کنت بک، ابداعکننده TDD، یک سیستم پرامپت منتشر کرده است که بر نوشتن سادهترین تست شکستخورده در ابتدا و حداقل پیادهسازی برای پاس شدن تاکید دارد. - اجرای مکانیکی: جدیدترین رویکرد استفاده از ابزارهایی مانند TDD Guard است. این ابزار هر بار نوشتن فایل را رصد (Hook) میکند و آن را مسدود میکند مگر اینکه فرآیند رعایت شده باشد (وجود تست شکستخورده، یک تست در هر بار، و کار به صورت Outside-in). این ابزار از یک مدل زبانی دوم بهعنوان داور استفاده میکند، زیرا سوال «آیا این کد از TDD پیروی کرده است؟» یک سوال مبهم (Fuzzy) است که پاسخ دادن به آن برای یک مدل راحتتر از یک قانون سختافزاری کدنویسی شده است.
با این حال، گزارش اشاره میکند که پرامپتنویسی به تنهایی اغلب منجر به توسعهی «تست-اول» (Test First) میشود، نه «تست-مدیریتشده» (Test-Driven). در حالت اول، عامل تمام تستها و سپس تمام کد را در دو بلوک بزرگ مینویسد. در حالی که ابزارهای فعلی توالی را کنترل میکنند (قرمز قبل از سبز)، اما کنترل نمیکنند که آیا تست حاصل واقعاً قابلیت شکست خوردن دارد یا خیر. این ناتوانی در تشخیص باگهای عمیق، ما را به یاد شکاف موجود در یافتن باگهای پیچیده میاندازد، جایی که تکیه بر استدلالهای مدل بدون تایید رسمی منجر به شکست میشود.

قوانینی برای تستهای هوشمندتر
برای جلوگیری از الگوهای شکست رایج، نویسنده پیشنهاد میکند قوانین خاصی در پرامپت سیستمی عامل گنجانده شود. این قوانین «شکل» تست را هدف قرار میدهند تا اطمینان حاصل شود که تست تمایزگذار (Discriminative) است. این موارد باید یکبار در دستورالعملهای عامل نوشته شوند تا از ایجاد اشکال توخالی جلوگیری شود:
- قرمزِ صرفاً ادعایی (Assertion-Only Red): فقط شکست در Assertion به معنای تست قرمز است. خطای کامپایل یا نبود یک نماد (Missing Symbol) تست شکستخورده محسوب نمیشود.
- اثبات منفی (Negative Proof): هر ادعای منفی باید ابتدا ثابت کند که کد اجرا شده است. ابتدا تعداد فراخوانیها (Call Count) را تایید کنید و سپس نبود آیتم را.
- ادعاهای دقیق (Specific Assertions): دقیقاً آیتم مورد نظر را تایید کنید، نه اینکه فقط بگویید یک مجموعه (Collection) خالی نیست.
- ساخت تولیدی (Production Construction): تستها باید از طریق سازندههای واقعی یا فکتوریهای تولیدی ساخته شوند و هرگز نباید از یک گراف اشیاء دستساز (Hand-built object graph) استفاده کنند.
- جداسازی وابستگیها (Dependency Isolation): کد مورد تست و کلاینتِ تست هرگز نباید یک وابستگی تزریقی (Injected Dependency) مشترک داشته باشند. از نمونهها (Instances) و رکوردکنندههای مجزا استفاده کنید.
- گزارشدهی مستند (Artifact Reporting): گزارش را از روی آرتیفکت ارائه دهید، نه از حافظه. عامل باید خطوط تغییر یافته را نقل کند یا خروجی خام تست را بچسباند.
- برنامهریزی تصمیممحور (Decision-Based Planning): برنامهها باید تصمیمگیرنده باشند. هیچ «یا این یا آن»، هیچ جایگذاری (Placeholder)، هیچ چکلیست از پیش تیکخوردهای نباشد و اصطلاحات پروژه باید دقیقاً از منبع نقل شوند.
نویسنده پیشنهاد میکند هرگاه بر سر پوشش (Coverage) یک تست اختلاف نظر وجود داشت، به جای بحث، موضوع را با یک «جهش» (Mutation) حل کنید.
بررسی جهش ۳۰ ثانیهای
قوانین اشتباهات رایج را میگیرند، اما نمیتوانند هر نقصی را پیشبینی کنند. هسته اصلی این متدولوژی، «بررسی جهش» (Mutation Check) است؛ یک فرم دستی از تست جهش که در زمان بازبینی (Review) انجام میشود، نه در CI.
بعد از اینکه تست سبز شد و قبل از رفتن به مرحله بعد، توسعهدهنده باید:
۱. یک خط از کد تولیدی را عمداً خراب کند.
۲. از پیش اعلام کند که کدام تست باید شکست بخورد.
۳. تست را اجرا کرده و تایید کند که در سطح Assertion شکست میخورد.
۴. خط را به حالت اول برگرداند.
در این فرآیند، کد در ابتدا درست است، یک نقص عمدی ایجاد میشود و شکست خوردن تست، نتیجه مطلوب است. در حالی که ابزارهای تست جهش مانند Stryker یا mutmut وجود دارند، استفاده از این روش به عنوان یک بررسی برای هر تغییر، تفاوت بین یک تست واقعی و یک «تزئینات کد» را مشخص میکند.
در مواجهه با عاملها، فاز «قرمز» اغلب گمراهکننده است. قرمز بودن اغلب ناشی از عدم وجود یک تابع است، نه شکست یک ادعا. یک تست قرمز ثابت میکند که قبل از وجود کد، شکست خورده است؛ اما بررسی جهش میپرسد: «آیا تست متوجه میشود اگر کد بعد از پیادهسازی خراب شود؟»
۶ الگوی رایج تستهای توخالی
نویسنده ۶ روش را شناسایی کرده که در آن تست پاس میشود اما عملاً هیچ چیزی را تست نمیکند (با استفاده از یک سرویس فرضی TypeScript):
۱. دادههای آزمایشی (Fixture) مانع شکست میشوند (قابل پیشگیری با قانون ۵): در تستی که یک سرویس ID همبستگی (Correlation ID) را فوروارد میکند، کلاینت تست و سرویس یک رکوردکننده مشترک دارند. چون درخواست ورودی از قبل هدر را دارد، رکوردکننده آن را میبیند، فارغ از اینکه سرویس واقعاً آن را فوروارد کرده یا خیر. خراب کردن منطق فوروارد، تست را سبز نگه میدارد. راه حل: استفاده از رکوردکنندههای مجزا و تایید در سمت تامینکننده.
۲. ساخت دستی گراف اشیا (قابل پیشگیری با قانون ۴): تست اشیا را با تنظیمات درست به صورت دستی میسازد به جای استفاده از فکتوری تولیدی. اگر فکتوری تولیدی ارسال یک تنظیم را متوقف کند، تست متوجه نمیشود چون هرگز آن فکتوری را صدا نمیزند. در بازبینی متقاعدکننده به نظر میرسد اما کدی که ارسال میشود را تست نمیکند.
۳. «چیز بدی ارسال نشد» در حالی که هیچچیز ارسال نشد (قابل پیشگیری با قانون ۲): استفاده از expect(rec.requests.some((r) => HEADER in r.headers)).toBe(false); اگر مسیر کد اصلاً اجرا نشود، پاس میشود. «اتفاق بدی نیفتاد» و «هیچ اتفاقی نیفتاد» یکسان به نظر میرسند. راه حل: ابتدا ثابت کنید فراخوانیها انجام شدهاند.
۴. «خالی نیست» برای چیزی که هرگز خالی نمیشود (قابل پیشگیری با قانون ۳): یک ردپای حسابرسی (Audit Trail) که همیشه شامل یک ورودی ورودی است، ادعای «خالی نبودن» را پاس میکند، حتی اگر رزرویشن خاص ثبت نشده باشد. راه حل: تایید دقیق ورودی خاص.
۵. ادعاهای به ظاهر زائد (غیرقابل پیشگیری با قانون): در تست توالی لاگ، ادعای دومی که چک میکند شمارنده بعد از یک نوشتنِ شکستخورده تغییر نکرده است، ممکن است زائد به نظر برسد. اما اگر شمارنده قبل از نوشتن افزایش یابد، فقط این ادعای «زائد» باگ را میگیرد.
۶. پنهان شدن رفتار در سیستمهای پاییندستی (غیرقابل پیشگیری با قانون): یک تست یکپارچگی سیستمی پاییندستی را چک میکند که IDهای تکراری والد را نادیده میگیرد و فقط اولی را نگه میدارد. ارسال ID در هر بار، تغییری در آن سیستم ایجاد نمیکند. تست درست است، اما رفتار از این زاویه نامرئی است؛ در اینجا یک تست واحد روی درخواستهای خروجی لازم است.
ریشهیابی و اصلاح
وقتی یک جهش زنده میماند (تست شکست نمیخورد)، غریزه ما این است که ادعا (Assertion) را تیزتر کنیم. اما گزارش استدلال میکند که این معمولاً اشتباه است. زنده ماندن جهش یعنی نتیجه تست به آن رفتار وابسته نبوده است. توسعهدهندگان باید این ۴ مورد را به ترتیب بررسی کنند:
۱. آیا مسیر کد اجرا شد؟ یک شمارنده فراخوانی یا دستور print اضافه کنید. اگر نه، تست به صورت توخالی (Vacuously) پاس شده است.
۲. آیا مقدار میتوانست از مسیر دیگری برسد؟ فیکسچرهای مشترک، متغیرهای جهانی یا مقادیر پیشفرض جایگزین را چک کنید.
۳. آیا ادعا روی چیزی است که همیشه درست است؟ فیلدهایی با مقادیر پیشفرض یا مجموعههایی که هرگز خالی نمیشوند را بررسی کنید.
۴. آیا این همان شیئی است که تولید (Production) میسازد؟ مطمئن شوید تست خودش شیء را سرهم نکرده است.
علت را اصلاح کنید، نه علامت را، و سپس جهش را دوباره اجرا کنید. بازنویسی تنها زمانی تمام شده است که تست قرمز شود.
تحلیل: تغییر نقش بازبین انسانی
این رویکرد نقش بازبین را از یک «خواننده کد» به یک «تستکننده جهش» تغییر میدهد. در جریان کاری سنتی، بازبین به یک تست سبز نگاه میکند و فرض میکند رفتار پوشش داده شده است. در جریان کاری عاملمحور، چراغ سبز نقطه شروع است، نه نتیجه نهایی.
این متدولوژی به مرحله برنامهریزی نیز گسترش مییابد. وقتی عاملها ابتدا برنامه مینویسند، خود برنامه یک آرتیفکت است که قابل تایید است. بازبینها باید به دنبال «برنامههای توخالی» باشند: تصمیماتی که به صورت «یا این یا آن» رها شدهاند، جایگذارهایی برای نام تستها، یا چکلیستهای از پیش تیکخورده. نویسنده حالت «فقط تایید» (VERIFY ONLY) را پیشنهاد میکند که در آن عامل برای هر بررسی در برنامه، PASS یا FAIL گزارش کرده و شماره خطوط دقیق را به عنوان مدرک نقل کند.
الگوهای هشداردهنده در رفتار عاملها
برخی رفتارهای تکراری عاملها سیگنالی برای بازبینی دقیقتر هستند:
- تعدیل یافتهها (Softened Findings): وقتی یک شکاف به عنوان «غیر مسدودکننده» (Not a blocker) توصیف میشود یا یک ادعا «زائد» خوانده میشود، آن را با یک جهش حل کنید.
- برداشت غلط از محدودیتهای سخت: وقتی «عدم ارتقای نسخه» به عنوان «عدم افزودن وابستگی جدید» تفسیر میشود، محدودیتها را دقیقاً بازگو کنید.
- گزارشهای حافظهمحور: وقتی عامل میگوید «همه تستها پاس شدند» بدون اینکه بعد از آخرین ویرایش آنها را اجرا کرده باشد، خروجی خام را مطالبه کنید.
- ایجاد شکاف (Gap Creation): وقتی یک اصلاح، فراخوانیای را جایگزین میکند که در سکوت یک مقدار پیشفرض تامین میکرد، بپرسید فراخوانی قدیمی چه چیزی ارائه میداد.
- بازنشانی زمینه (Context Reset): وقتی یک انتخاب تثبیت شده در یک جلسه جدید دوباره مورد بحث قرار میگیرد، از یادداشتهای تحویل (Handoff notes) برای حفظ تاریخچه استفاده کنید.
چه زمانی از این روش استفاده کنیم و چه زمانی نه
بررسیهای جهش ثابت میکنند که یک تست میتواند برای یک تغییر شکست بخورد، اما ثابت نمیکنند که مجموعه تستها کامل است یا یک اصلاح، جای دیگری را خراب نکرده است. انجام این کار برای کارهای کوچک و مشهود (مانند تغییرات متنی یا قوانین اعتبارسنجی ساده) که در آن Diff کد بدیهی است، ارزشمند نیست.
زمانی به سراغ تست جهش بروید که کد دارای «غیرمستقیم بودن» (Indirection) باشد: وابستگیهای تزریقی، فکتوریها، سیستمهای پاییندستی و مسیرهای ناهمگام (Async). اینها مناطقی هستند که تستهای توخالی در زمان بازبینی نامرئیترین حالت خود را دارند.
گام بعدی شما
توسعهدهندگان میتوانند این حالتهای شکست را با استفاده از پروژه نمونه در گیتهاب (https://github.com/thasnim-fluxone/tests-that-cannot-fail) تست کنند تا ببینند چگونه تستهای توخالی از جهش جان سالم به در میبرند. اجرای npm run mutate هر تست توخالی که زنده مانده و هر تست اصلاحشدهای که شکار شده است را نشان میدهد. منتظر ظهور ابزارهای جهش خودکار باشید که مستقیماً در حلقههای عاملمحور ادغام شوند تا جایگزین بررسی دستی ۳۰ ثانیهای شوند.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو