ما الذي يمكنني حذفه وتبقى الاختبارات خضراء؟
اطلب من وكيل كتابة الاختبارات، فيكتب بدائل وهمية وانتظارات وفحوصاً لوجود الأزرار. سؤال واحد لكل اختبار يحوّل تلك القائمة إلى مجموعة اختبارات تستحق البقاء.

سدّ الجزء السابق آخر فجوة في خطة الترحيل. وتبقى خطوة واحدة في كل مرحلة منها: الاختبارات التي يكتبها الوكيل. خذ مكوّن التحويل الجديد من الجزء 4، واطلب من الوكيل اختباراته. وهذا ما يعود عادةً:
- نسخة وهمية () من
AccountsStore({ accounts: () => [...] } as any)، ونسخة وهمية منRouter await new Promise((r) => setTimeout(r, 0))بين الخطوات- توقعات () على
component.error()وcomponent.verdict() - اختبار يتحقق من أن
querySelector('button')لا يساوي
اختبارات معزولة، وبدائل وهمية في كل مكان، وتحقق من تفاصيل التنفيذ. يميل الوكيل تلقائياً إلى عكس ما تكون عليه مجموعة اختبارات جيدة. لذلك اطرح سؤالاً واحداً على كل اختبار.
السؤال
ما الذي يمكنني حذفه من كود الإنتاج وتبقى النتيجة خضراء؟
اختبارات خضراء، وكود نظيف، ونشر سريع، ثم لا أحد يستطيع أن يقول ما الذي يفعله الكود حين يتعطل الإنتاج.
اختبار «الزر موجود». يمكنني حذف مسار الإرسال بأكمله ويبقى الاختبار أخضر. احذف الاختبار.
«الحساب المحظور يعرض رسالة»، مع مخزن () وهمي. المخزن الوهمي هو الذي يعيد الرسالة، لذا يمكنني تجاوز canTransfer() ويبقى الاختبار أخضر. لكن الحساب المحظور متطلب يراه العميل. أعد كتابة الاختبار بالمخزن الحقيقي، وتحقق من ظهور الرسالة على الشاشة.
«ينتقل بعد التحويل»، مع مخزن وهمي وموجّه () وهمي. يمكنني حذف استدعاء store.transfer() ويظل الموجّه الوهمي يسجّل الانتقال. أعد كتابته بالمخزن الحقيقي فوق واجهة خلفية مزيفة ()، ثم تحقق من أمرين: أن الرصيد نقص، وأن المسار تغيّر.
انتظارات setTimeout. أعد كتابتها باستخدام await fixture.whenStable().
في تجارب الورشة على هذا المكوّن، حُذف نحو نصف الاختبارات المولَّدة، وأُعيدت كتابة ربعها عبر الـ ، وبقي الربع. المسودة الأولى التي يكتبها الوكيل لمجموعة الاختبارات قائمة اختبارات مرشّحة، ومراجعتها هي ما يستهلك الوقت.
قائمة العلامات الحمراء
هذه السطور تدخل ملف AGENTS.md في المستودع التجريبي، كي يكون الجيل التالي من الاختبارات أفضل قبل أن يراجعه أحد:
- بديل اختبار () لأي شيء داخل الوحدة؛ الحدّ الخارجي وحده، أي الواجهة الخلفية المزيفة، يحصل على بديل مزيف
- انتظار مؤقت () أو
setTimeoutداخل اختبار - تحققات على حقول المكوّن بدل الـ DOM أو حالة المخزن
- قيمة متوقعة يعيد حسابها الكود نفسه الذي يخضع للاختبار، بدل أن تُكتب صراحة
- عنصر يُتحقق من وجوده وهو موجود منذ أول عرض للمكوّن () أياً كانت البيانات
- تشغيل أخضر تظهر في مخرجات الـ الخاصة به رسالة خطأ
البند الأخير هو الأرخص في الفحص والأكثر إهمالاً. فظهور خطأ عابر من الواجهة الخلفية في تشغيل أخضر يعني عادةً أن اختباراً نجح دون أن يتحقق مما حدث من خطأ.
اقرأ قواعد الاختبار في AGENTS.md. ثم راجع libs/feature/transfers/src/lib/new-transfer.spec.ts، وهو ملف كتبته أنت. لكل it()، أجب عن سؤال واحد: أي سطر من كود الإنتاج يمكن حذفه ويبقى هذا الاختبار أخضر؟ إذا كان الجواب «أي سطر مهم»، فضع على الاختبار علامة DELETE أو REWRITE، واذكر القاعدة التي يخالفها. لا تعدّل الملف. أجب في جدول: اسم الاختبار، والحكم، والقاعدة.
الوكيل حَكَم منصف على اختباراته حين يكون السؤال بهذه الدقة. وكثيراً ما يصنّف عدداً كبيراً منها بنفسه، فتتحول مراجعتك إلى تدقيق في أحكامه.
كيف يبدو الاختبار الذي يبقى
من new-transfer.spec.ts على الفرع track1/m2-ngmodule-solution:
it('moves the money and returns to the account', async () => {
const { harness, type, click, store, router } = await open();
type('Amount (MAD)', '500');
type('Label', 'Loyer');
await click('Confirm transfer');
await harness.fixture.whenStable();
harness.detectChanges();
expect(store.accounts()[0].balanceMad).toBe(1500);
expect(router.url).toBe('/accounts/a-1');
});تفتح open() الرابط مع المخزن الحقيقي فوق واجهة خلفية مزيفة تستجيب فوراً، ويبدأ فيها الحساب بـ 2,000 درهم مغربي. تجد type() الحقل من اسمه الظاهر، وتجد click() الزر من اسمه، كما يفعل المستخدم. إذن 1500 هو المتطلّب مكتوباً: 2,000 ناقص الـ 500 المكتوبة. احذف استدعاء store.transfer() من المكوّن، وسيصبح هذا الاختبار أحمر. هذا هو التعريف الكامل للاختبار الذي يستحق البقاء.
العودة إلى الاجتماع
بدأ الجزء 1 بـ يتجاوز ألف سطر بكثير، وباجتماع لم يكن يُحسب فيه إلا العرض التوضيحي. وهذا النوع نفسه من الترحيل في المستودع التجريبي، مقيساً بالأمر git diff --stat على الفرع track1/m2-ngmodule-solution ومحسوباً لكل ملف على حدة:
- التثبيت (المهمة 000): 141 سطراً، كلها اختبارات. لم يتغير أي سطر من كود الإنتاج.
- البناء بجانب القديم وتبديل المسار (المهمة 002): 233 سطراً من كود الإنتاج، معظمها مكوّن واحد من 223 سطراً، إضافة إلى 271 سطراً من الاختبارات والملفات الأولية التي أنشأها المولّد () في . أما التبديل نفسه فسطر واحد تغيّر.
- الحذف (المهمة 003): حُذف 545 سطراً وأُضيف سطر واحد، بلا أي منطق.
ثلاثة ملفات مهام، كل منها طلب دمج () يستطيع شخص قراءته في جلسة واحدة، ولا خطوة تحتاج إلى أن يتذكر أحد كيف كانت الشاشة القديمة تعمل. وبُنيت شاشة من الجزء 6 بجانب القديمة وحُوّل مسارها بالطريقة نفسها على الفرع track1/m2b-angularjs-solution، وانتهت بتغيير في المسار من أربعة أسطر. هذه أرقام المستودع التجريبي، لا أرقام عميل. أما أرقامك فيعطيك إياها الأمر نفسه.
طلب الدمج الذي تقرؤه في جلسة واحدة تستطيع أن تشرحه في الثانية فجراً.
مع بداية الأسبوع
- لكل اختبار مولَّد، اسأل: ما الذي يمكنك حذفه وتبقى النتيجة خضراء؟ ثم احذف أو أعد الكتابة بحسب الجواب.
- ضع العلامات الحمراء في ملف قواعدك.
- شغّل
git diff --statعلى طلبات الدمج الثلاثة التالية التي يكتبها الوكيل، واكتب بجانب كل منها ساعات المراجعة. خذ هذين العمودين إلى الاجتماع، لا العرض التوضيحي. فهما يجيبان عن سؤال الجزء 1: لماذا لسنا أسرع؟
التمرين: المشهد الثالث من الوحدة 2 في المستودع التجريبي. ابدأ من فرعك في المشهد الثاني، أو بالأمر git switch -c m2/tests origin/track1/m2-ngmodule-solution. الحل المرجعي: الاختبارات الخمسة في new-transfer.spec.ts على ذلك الفرع. ثم احذف قواعد الاختبار من AGENTS.md، وشغّل التمرين مرة أخرى، ولاحظ الفرق.
المسار 1، الجزء 7 والأخير من هذا المسار، في السلسلة المجانية المرافقة لورشة سريع وآمن: الذكاء الاصطناعي في قاعدة كود Angular الخاصة بك. أما الجزء الختامي فهو السياسة التي يكتبها فريقك.
سريع وآمن: الذكاء الاصطناعي في كود Angular الخاص بكم
دع الوكلاء يكتبون Angular تحت قواعد، وحدّث الكود القديم دون أن تكسر الإنتاج، وحافظ على أمانه، في ثلاثة أيام، على كود بحجم المؤسسات.
اطّلع على البرنامج