هذا الفيديو عن الاختبارات. سنعرّف ما هو الاختبار فعلًا، وما الذي يضيفه الـ assertion، ونشغّل مجموعة اختبارات ونقرأ تقريرها، ونحوّل خطأً برمجيًا إلى اختبار قبل إصلاحه. ثم سنطلب من وكيل أن يكتب اختبارات، وننظر في أنواع الاختبارات التي يميّز المطوّرون بينها. وسنجرّب أيضًا كتابة اختبار قبل كتابة الكود.
كلمة «اختبار» تتكرّر كثيرًا عندما نعمل مع وكلاء البرمجة، لذا لنعرّفها. الاختبارات في هذا الفيديو برامج تشغّل كودكم على مُدخَل اخترتموه مسبقًا. مثالنا دالة اسمها initials، تأخذ اسمًا مثل Alan Turing وتعيد AT.
تشغيل الدالة يُظهر ما الذي أعادته. ولنحكم على النتيجة، نحتاج أيضًا إلى الإجابة التي توقّعناها، مكتوبةً قبل أن نشغّل أي شيء.
يقارن الاختبار بين الاثنين. إذا تطابقا، ينجح الاختبار بصمت. وإذا اختلفا، يفشل، ويبلّغ مشغّل الاختبارات عن عدم التطابق.
في هذه المقارنة نرى أداة صورية (formal) تعمل. فهي تطرح سؤالًا واحدًا محدّدًا وتجيب عنه بدقة. الغاية من الاختبار أنه يمكن أن يفشل. عندما تكتبون اختبارًا لأول مرة، شاهدوه يفشل للسبب الذي تتوقّعونه. هذا يُثبت أنه يفحص فعلًا السلوك الذي قصدتموه.
هذه محاولة أولى، بلغة Python باستخدام pytest، وبلغة TypeScript باستخدام vitest. لا شيء في هذا الفيديو يعتمد على اللغة التي تستخدمونها. كلا الاختبارين يستدعي initials ويطبع ما تعيده. لو أعادت initials القيمة ZZ، فهل سيفشل أيٌّ من الاختبارين؟
لا يقارن أيٌّ منهما النتيجة بأي شيء. ولا يفشل إلا إذا انهار الاستدعاء. ولو أعادت initials حروفًا خاطئة، لنجح الاختباران مع ذلك.
الـ assertion يضيف المقارنة الناقصة. فهو يقارن القيمة المُعادة بالقيمة المتوقّعة، ويُفشل الاختبار عندما تختلفان.
طباعة النتيجة مفيدة أثناء الاستكشاف أو تصحيح الأخطاء. أما الـ assertion فيحوّل السلوك المتوقّع إلى فحص يقيّمه مشغّل الاختبارات عنكم.
إذن كيف تشغّلون اختباراتكم فعلًا؟ على الأرجح فعلتم ذلك في مساقات سابقة، فإليكم تذكيرًا قصيرًا. في Python، شغّلوا pytest. وفي TypeScript، شغّلوا npx vitest run. كل مشغّل يجد الاختبارات التي تحدّدها إعدادات المشروع ويشغّلها. وإذا احتجتم إلى أكثر من هذا التذكير، فاطّلعوا على توثيق pytest أو vitest.
ويعيد المشغّل أيضًا حالة خروج (exit status). الصفر يعني أن كل الاختبارات نجحت، وأي قيمة غير الصفر تعني أن شيئًا ما فشل. وتستطيع أوامر أخرى أن تعتمد على هذه الحالة. في هذا السطر، الرمز && يشغّل git push فقط إذا أعاد pytest صفرًا، لذلك لا يحدث الـ push عندما يفشل أحد الاختبارات. يستطيع الوكيل استخدام هذه الحالة ليقرّر هل يتابع، ثم يقرأ تقرير الفشل ليعرف ما الذي يحتاج إلى إصلاح.
أثناء إصلاح شيء واحد، من المفيد أن يتوقّف التشغيل عند أول فشل. في pytest يكون ذلك بالخيار -x، وفي vitest بالخيار --bail 1.
هذا تقرير pytest مختصر فيه اختبار واحد فاشل. السطر الأول يذكر الاختبارات التي اختارها المشغّل. لقد جمع اختبارين. بعده تأتي علامة لكل اختبار: نقطة للنجاح وحرف F للفشل. وعند الفشل، يعرض التقرير الـ assertion الذي لم يتحقّق، مع القيمتين. هنا أعادت initials الحرفين بأحرف صغيرة، بينما توقّع الاختبار أن يكونا بأحرف كبيرة. الأسطر الأخيرة تذكر اسم الاختبار الفاشل وتعدّ النتائج. وتقرير vitest يتضمّن الأجزاء نفسها.
يمكن أن يُظهر التقرير أيضًا خطأً بدلًا من فشل. هنا يستورد ملف الاختبار وحدة (module) اسمها names غير موجودة، لذلك لم يتمكّن المشغّل حتى من تحميل الاختبارات.
والاثنان يعنيان أمرين مختلفين. الفشل حكم على الكود قيد الاختبار. أما خطأ جمع الاختبارات فمشكلة إعداد، ولم يُشغَّل أي اختبار، لذلك لا يقول التقرير شيئًا بعدُ عن initials. أصلحوا الإعداد أولًا.
والآن لنستخدم اختبارًا لإصلاح خطأ برمجي. لنفترض أن أحدهم كتب اسمًا بمسافتين بين الكلمتين، فانهارت initials. التقسيم عند مسافة واحدة يترك كلمة فارغة، وأخذ حرفها الأول يُطلق خطأ فهرس (IndexError).
قبل إصلاح الخطأ، اكتبوا أصغر اختبار يُعيد إنتاجه، وتحقّقوا من أنه يفشل للسبب المتوقّع. هنا يفشل الاختبار بخطأ الفهرس نفسه، فهو إذن يعيد إنتاج الانهيار. وهذا يعطيكم حالة ملموسة لإصلاحها. ويعطي الوكيلَ أيضًا شيئًا يشغّله في حلقة. يستطيع الوكيل أن يعدّل الكود ويعيد الاختبار، ويواصل حتى ينجح الاختبار.
ثم تُصلحون الكود. التقسيم عند أي عدد من المسافات المتتالية يُسقط الكلمة الفارغة. الاختبار الجديد يصبح أخضر، والاختبار الأصلي ما زال ينجح.
بعد أن ينجح الاختبار، أبقوه في مجموعة الاختبارات ليكشف عودة الخطأ نفسه لاحقًا. يُسمّى اختبار تراجع (regression test). ومع إصلاحكم للأخطاء، تبنون سجلًّا من الحالات التي يجب أن تظل تعمل.
لننظر الآن كيف نطلب من الوكيل كتابة اختبارات. طلب كهذا يترك له أن يقرّر النطاق ومعنى السلوك الصحيح معًا.
لنفترض أنه أعاد 40 اختبارًا. عليكم الآن قراءتها لتعرفوا ما الذي تغطّيه، والطلب لم يعطكم أساسًا يُذكر للحكم على ما هو مهم.
الطلب الأفضل يفعل ثلاثة أشياء. يذكر ما يجب أن يبقى صحيحًا بدل أن يطلب اختبارات، ويسمّي الحالات التي تهمّكم، ويطلب أصغر حالة فاشلة أولًا. بطلب مثل هذا، تحصلون من الوكيل على اختبار المسافتين.
أنتم تقرّرون أي الخصائص مهمّة، وتحسمون الغموض في المتطلّبات. يستطيع الوكيل أن يقترح حالات ويكتب الاختبارات، لكن عليكم مراجعة الإجابات المتوقّعة. والطلب المحدّد يجعل هذه المراجعة أسهل بكثير.
كتابة الاختبار قبل الكود ممارسة معروفة. تُسمّى التطوير الموجَّه بالاختبارات، أو TDD. وهي تتطلّب الكثير من الانضباط، ووجدها كثير من المطوّرين مُرهقة جدًا للمواظبة عليها يدويًا. مع وكيل، يمكنكم أن تطلبوا كل خطوة وتتحقّقوا من أنها حدثت. كل سلوك صغير يمرّ بثلاث خطوات. أولًا، الأحمر. تكتبون الاختبار قبل أن يوجد الكود، ثم تشغّلونه وتشاهدونه يفشل. اطلبوا من الوكيل أن يعرض عليكم ذلك التشغيل الفاشل ويشرح سبب فشله. يجب أن يفشل لأن السلوك غير موجود، لا بسبب خطأ في الإعداد مثل الوحدة الناقصة التي رأيناها سابقًا.
ثانيًا، الأخضر. تكتبون أقل قدر من الكود يجعل ذلك الاختبار ينجح. لا التصميم الذي تتخيّلون أنكم ستحتاجونه لاحقًا، بل ما يكفي فقط ليصبح أخضر. اطلبوا من الوكيل أن يريكم التشغيل الناجح والـ diff لما غيّره.
ثالثًا، إعادة الهيكلة (refactor). رتّبوا البنية مع الحفاظ على السلوك. الاختبارات تحمي الحالات التي تغطّيها بينما تعيدون ترتيب الكود، وهذا ما يجعل إعادة الهيكلة بهذه الطريقة أكثر أمانًا.
ثم تبدأ الدورة من جديد مع السلوك التالي. في initials، قد تكون إحدى الدورات حالة المسافتين، والتالية قد تكون اسمًا مكتوبًا بأحرف صغيرة. كل تغيير صغير يبدأ باختبار فاشل لسلوك نريده. والمجموعة المتنامية تحفظ تلك الحالات أثناء إعادة الهيكلة. ما زال علينا أن نبحث عن مُدخَلات وتركيبات من الميزات لم نختبرها. الاختبار الذي لم تروه يفشل أبدًا هو اختبار لم تختبروه. شاهدوه يفشل للسبب المتوقّع قبل أن تعتمدوا على نتيجة النجاح.
ويزداد الترتيب أهمية مع الوكيل. إذا جاءت الاختبارات بعد الكود، يستطيع الوكيل أن يقرأ الكود ويستخدمه ليحدّد النتيجة المتوقّعة.
وإذا نسخ سلوكًا خاطئًا إلى الإجابة المتوقّعة، ينجح الاختبار بينما يبقى الخطأ. عندها يتّفق الكود والاختبار لأنهما يشتركان في سوء الفهم نفسه.
لذا قسّموا العمل. استنتجوا الإجابة المتوقّعة من المتطلّبات، أي مما قرّرتم أن يفعله البرنامج. أنتم تكتبون الاختبار الفاشل، أو تقرؤون الاختبار الذي اقترحه الوكيل وتوافقون عليه. ثم لا يكتب الوكيل إلا الكود الذي يجعله ينجح، وتقرؤون أنتم الـ diff. يقوم الوكيل بالجزء الذي يُنجزه بسرعة، وتحتفظون أنتم بالجزء الذي يحدّد ما هو الصحيح.
كتابة الاختبار أولًا لا تمنع كل خطأ مشترك. إذا أساء الوكيل فهم الطلب، فالاختبار الذي يقترحه والكود الذي يكتبه قد يحملان التوقّع الخاطئ نفسه. لهذا تراجعون الإجابة المتوقّعة في ضوء المتطلّبات. والاختبار الذي راجعتموه يعطيكم أيضًا مُقيِّمًا للعمل الجديد. فقبل أن يوجد الكود، تكونون قد حدّدتم الإجابة التي يجب أن يُنتجها لهذه الحالة.
توقّعوا هنا نمط فشل محدّدًا. إذا أعطيتم وكيلًا اختبارًا لا يستطيع اجتيازه، فأمامه طريقتان ليجعل التشغيل أخضر.
الأولى أن يُصلح الكود، وهذا ما أردتموه. والثانية أن يغيّر الاختبار، وهذا أيضًا يجعله أخضر.
إليكم مثالين. في الأول، لم يُمسّ ملف الاختبار، والكود يعيد AT لهذا المُدخَل وحده. في الثاني، صار اختبار المسافتين يستخدم مسافة واحدة، فلم يعد يختبر الخطأ. لوحظت نماذج تحذف الـ assertions، أو تكتب النتائج ثابتة في الكود، أو تعالج مُدخَل الاختبار كحالة خاصة بدل تنفيذ السلوك العام.
لذلك عندما يبلّغ الوكيل أن الاختبارات تنجح، اقرؤوا diff الاختبارات إلى جانب diff الكود، وابحثوا عن حالات خاصة في الكود. تغيير التوقّع مبرَّر عندما تتغيّر المتطلّبات. أما إذا بقيت المتطلّبات كما هي، فالاختبار المُضعَف لا يفعل سوى إخفاء الفشل.
والآن بعد أن صار لدينا إجراء، لننظر كيف يصنّف الناس الاختبارات. أحد التصنيفات الشائعة هو حسب مقدار ما يشغّله الاختبار من النظام. اختبار الوحدة (unit test) يفحص جزءًا صغيرًا وحده، مثل دالة واحدة تُستدعى مباشرةً في الذاكرة. اختبار وحدة بسيط مثل اختبارات initials يمكن أن يُنفَّذ في ميلي ثانية أو أقل، ونطاقه الصغير يساعدكم على تحديد مكان الفشل.
اختبار التكامل (integration test) يجمع عدة أجزاء معًا، ويتحقّق من أنها ما زالت تعمل مجتمعةً، ككودكم مع قاعدة بيانات حقيقية. يجب أن تكون قاعدة البيانات قيد التشغيل ومُعدّة قبل بدء الاختبار، وعند الفشل هناك أكثر من مشتبه به.
اختبار end-to-end يستخدم التطبيق بأكمله عبر واجهته الخارجية، كما يصل إليه مستخدم أو برنامج آخر، مثلًا بالتنقّل بين الصفحات بالنقر في متصفح. قبل أن يبدأ اختبار المتصفح، يجب أن يكون الخادم وبياناته والمتصفح جاهزة. ويمكن أيضًا أن يفشل لسبب غير خلل في السلوك الذي أردتم فحصه، مثل رسوم متحركة ما زالت تعمل.
ابدؤوا بقواعد البرنامج، حيث توجد حالات كثيرة يجب فحصها، واستدعاء الدوال مباشرةً يجعل ذلك قليل الكلفة. ثم أضيفوا اختبارات أوسع للروابط بين هذه القواعد والواجهة. اصرفوا جهد الاختبار حيث يكون الخطأ مرجّحًا أو مكلفًا، باستخدام أصغر اختبار يمكنه اكتشافه.
هناك تصنيفان آخران تجدر معرفتهما. وهذه التصنيفات تتداخل. اختبار التراجع الخاص بالمسافتين هو أيضًا اختبار وحدة، واختبار صغير بالمعنى الذي سنراه بعد قليل. تصنّف Google الاختبارات حسب ما يُسمح لها بالوصول إليه، لا حسب الطبقة. الصغير: عملية واحدة، بلا شبكة، وبلا نظام ملفات. المتوسط: جهاز واحد و localhost. والكبير قد يصل إلى جهاز آخر.
ويختلف الناس أيضًا في عدد الاختبارات من كل نوع في مجموعة الاختبارات، ولإجابتين منها اسمان معروفان. أحد النهجين يُسمّى هرم الاختبارات. فيه اختبارات وحدة كثيرة في القاعدة، واختبارات تكامل أقل فوقها، واختبارات end-to-end قليلة جدًا في القمة. والآخر يُسمّى كأس الاختبارات. قاعدته التحليل الساكن، الذي يفحص الكود دون تشغيله، مثل أدوات فحص الأنواع والـ linters. وفوق ذلك، يصرف معظم الجهد على اختبارات التكامل.
لا يصلح أيٌّ من الشكلين لكل مشروع. الشكل قرار بشأن المكان الذي تصرفون فيه جهد الاختبار. وهو يوازن بين المخاطر في كل جزء من النظام، وكلفة كتابة وتشغيل كل نوع من الاختبارات، ومدى سهولة انكسار هذه الاختبارات عندما يتغيّر الكود.
هناك أنواع كثيرة جدًا من الاختبارات. ولتكوين فكرة عن المجال، لنذكر بعضها. معرفة الأسماء تساعدكم على تمييز تقنية عندما يقترحها أحدهم، وعلى طلبها من الوكيل عندما تحتاجونها. اختبار الخصائص (property-based) يحدّد قاعدة يجب أن تتحقّق، ويولّد مُدخَلات تحاول كسرها. والـ fuzzing يفعل مثله، بحثًا عن الانهيارات. اختبارات snapshot و golden تقارن بمُخرَجات محفوظة ومعتمدة.
اختبارات التراجع المرئي تقارن لقطات الشاشة بصور محفوظة. اختبارات الأداء والحِمل تقيس السرعة تحت أعباء عمل مختلفة، واختبارات التحمّل (soak) تعمل مدة أطول. اختبارات العقود (contract) تتحقّق من اتفاق الخدمات على واجهتها.
الفحص الأمني يبحث عن مشكلات في الكود والاعتماديات والمعلومات السرّية المكشوفة. هندسة الفوضى (chaos engineering) تُعطّل البنية التحتية عمدًا لفحص التعافي. فحوصات إمكانية الوصول تنظر في مسائل، مثل الوصول بلوحة المفاتيح وما يتلقّاه قارئ الشاشة.
التحقّق الصوري يُثبت خاصيّة لكل الحالات في نموذج للنظام، بدل أخذ عيّنات من المُدخَلات. في الاختبار الاستكشافي اليدوي، يجرّب شخصٌ النظام بحثًا عن مشكلات لا تغطّيها الاختبارات الآلية. كل نوع من هذه يعالج خطرًا محدّدًا. مثلًا، المحلّل (parser) الذي يقبل مُدخَلات من غرباء مرشّح جيد للـ fuzzing. يكفي الآن أن تعرفوا أنها موجودة، وأن تلجؤوا إلى أحدها عندما تستطيعون تسمية الخطر الذي يعالجه.
نأمل أننا أقنعناكم بأن الاختبار يستحق الجهد. الآن يمكنكم أن تعطوا الوكيل سلوكًا محدّدًا ليختبره، وتتحقّقوا مما إذا كان اختباره يكتشف الخطأ. جرّبوا ذلك، واستمتعوا بكتابة الاختبارات!