تمنح الاختبارات الوكيلَ فحوصات لعمله يمكن تكرارها. ويتناول هذا الفيديو كلفة هذه الفحوصات. مجموعة الاختبارات كود عليكم تشغيله ومراجعته وصيانته. وبعض الاختبارات تستحق التحسين أو الحذف. سنتناول هذه التكاليف، ومنها الاختبارات الهشّة والاختبارات المتقلّبة والـ mocks. ثم سنُعطّل قاعدة عمدًا لنرى هل تكتشف الاختبارات ذلك، وسننظر في اختبار الطفرات وتغطية الأسطر، وكيف نقرّر أيّ الاختبارات نُبقي عليها.
مجموعة الاختبارات كود. فهي تُقرأ وتُعاد هيكلتها وتُصلَح، تمامًا كالبرنامج الذي تفحصه. تمنحنا الاختبارات فحوصات يمكن تكرارها للسلوك المتوقَّع. ولتبقى التغذية الراجعة مفيدة، علينا أن نصون مجموعة الاختبارات كما نصون الكود نفسه.
والآن غيّروا سلوكًا واحدًا. ستعدّلون البرنامج، لأن هذا هو التغيير الذي قصدتموه.
وقد تحتاجون أيضًا إلى تعديل اختبارات وصفت السلوك القديم. لذلك قد يعني تغيير واحد في المتطلبات عملًا في البرنامج وفي الاختبارات معًا.
عندما تقدّرون حجم تغيير، احسبوا معه الاختبارات التي تحتاج إلى تحديث ومراجعة. فهي جزء من الكود الذي تصونونه.
تابعوا اختبارًا واحدًا عبر دورة حياته. تكتبونه مرة واحدة، وفي يوم كتابته تكون هذه كلفته كلها.
بعد ذلك يُشغَّل الاختبار مع كل تغيير، ما دام باقيًا في مجموعة الاختبارات. ويقرؤه أحدهم في كل مرة يفشل فيها، ويحدّثه في كل مرة يتغيّر فيها السلوك الذي يصفه. معظم كلفة الاختبار تأتي بعد كتابته. لذا عندما تضيفون اختبارًا، فأنتم تلتزمون أيضًا بصيانته.
اختبار كاشف التغيير (change-detector) مرتبط بالتنفيذ ارتباطًا وثيقًا، حتى إن إعادة هيكلة غير ضارة تجعله يفشل. إليكم اختبارًا لسلة تسوّق. يضيف كتابًا ثمنه 12، ثم يقرأ مباشرة القائمة الداخلية لعناصر السلة. الشرطة السفلية في بداية الاسم تدلّ على أن هذه القائمة داخلية في السلة. طريقة تخزين السلة لعناصرها خيار داخلي. فالمستخدم لا يرى إلا ما في السلة وكم تكلّف.
والآن غيّروا السلة لتحفظ عناصرها في قاموس بدل قائمة. السلة تحوي الكتاب نفسه، والمجموع ما زال 12، ولن يلاحظ أي مستخدم أي فرق.
ومع ذلك يفشل الاختبار. لم يكن يفحص أصلًا ما تحويه السلة أو كم تكلّف. من دون قصد، كان يفحص أن السلة تحفظ عناصرها في قائمة. وهذا اختيار داخل الكود، ولا يراه أي مستخدم.
تصف Google اختبارات كهذه بأن قيمتها سلبية. فتغيير داخلي غير ضار يتطلّب الآن تحديث الاختبار أيضًا، دون أن يخبركم بأن أي سلوك قد تعطّل.
كِنت بيك روّج للتطوير الموجَّه بالاختبارات، أي دورة أحمر، أخضر، إعادة هيكلة من هذا الأسبوع، وله قاعدة قصيرة. اربطوا الاختبارات بالسلوك، وأبقوها مستقلة عن البنية الداخلية. الشريط الأزرق على الحافة اليسرى للبرنامج هو واجهته العامة، أي الدوال أو واجهة الوحدة التي يستخدمها مَن يستدعيه. والاختبار الجيد يستدعي البرنامج من خلالها. وهذا لا يعني أن على كل اختبار أن يفتح متصفحًا.
أما الاختبار الآخر فيعتمد على ترتيب داخلي واحد بعينه، وهذا هو معنى الاقتران بالبنية. فهو يتجاوز الواجهة العامة ليفحص كيف نُظِّم العمل.
والآن أعيدوا تنظيم الكود مع الحفاظ على سلوكه العام. الاختبار الذي يستخدم الواجهة العامة ما زال ينجح. أما الاختبار المرتبط بالترتيب الداخلي القديم فيفشل، مع أن السلوك لم يتغيّر.
اربطوا الاختبارات بالسلوك وافصلوها عن البنية. الاختبارات المكتوبة بهذه الطريقة تتيح لكم إعادة هيكلة التنفيذ مع الاستمرار في فحص ما يحتاج إليه مَن يستدعيه.
لنعد إلى السلة. الاختبار الأول دخل إلى القائمة الداخلية للسلة، لذلك جعله تغيير غير ضارّ يفشل. أما الاختبار الثاني فيستدعي الدوال العامة للسلة، items و total، كما يفعل باقي البرنامج. احفظوا العناصر داخليًا في قائمة أو في قاموس، وسينجح في الحالتين. لا يفشل إلا عندما يتغيّر السلوك، وهذا هو الفشل الذي تريدون أن يبلّغ عنه الاختبار.
الاختبار المتقلّب ينجح أحيانًا ويفشل أحيانًا دون أي تغيير في الكود. وهذا يجعل تفسير الفشل أصعب. في 2016، أفادت Google أن نحو 1.5% من مرات تشغيل الاختبارات أعطت نتائج متقلّبة، وأظهر نحو 16% من الاختبارات شيئًا من التقلّب.
ونحو 84% من الانتقالات المرصودة من النجاح إلى الفشل تعلّقت باختبار متقلّب. وكثيرًا ما تطلّب الفشل تحقيقًا للتمييز بين تراجع حقيقي وتشغيل غير موثوق للاختبار.
إذا صارت إعادة التشغيل هي الردّ المعتاد على الفشل، فقد يتوقّف الناس عن البحث فيما حدث من خطأ. الاختبارات غير الحتمية قد تقوّض الثقة في مجموعة الاختبارات كلها.
لذا أصلحوا السبب. إذا كان الاختبار يعتمد على الوقت الحالي أو على مُدخَلات عشوائية، فتحكّموا فيهما، مثلًا بتمرير ساعة ثابتة أو بذرة عشوائية (seed) ثابتة. وإذا كان ينتظر عددًا ثابتًا من الثواني حتى يحدث شيء ما، فاجعلوه ينتظر حتى يتحقّق الشرط. وبينما تعملون على الإصلاح، يمكنكم عزل الاختبار حتى لا يعرقل عمل الجميع، ثم إعادته عندما يصبح موثوقًا.
وقد تبطؤ مجموعة الاختبارات حتى تتوقّفوا عن تشغيلها أثناء التعديل. فانتظار 20 دقيقة يُبعد التغيير عن التغذية الراجعة عليه. لكن المجموعة البطيئة قد تستحق الإبقاء عليها. شغّلوا بعد كل تعديل مجموعة سريعة من الاختبارات، والمجموعة الكاملة بوتيرة أقل، مثلًا قبل الدمج.
كتابة الاختبارات يدويًا تستغرق وقتًا، ويستمر العمل بعد ذلك في مراجعتها وصيانتها.
يستطيع الوكيل أن يكتب بسرعة مسودّة لمئات الاختبارات، ربما 400 قبل الغداء. لكن ما زال عليكم أن تقرؤوها، لأن الاختبار قد ينجح وهو يفحص الشيء الخطأ. قد يدخل إلى تفاصيل الكود الداخلية، مثل اختبار السلة. أو قد يفحص ما يفعله الكود اليوم، بما فيه من أخطاء برمجية.
وكثير من الجهد ينتقل الآن من كتابة الاختبارات إلى مراجعتها. التوليد السريع قد يترك لكم مئات الاختبارات لقراءتها وصيانتها، لذا فحجم الكود المولَّد ليس مقياسًا لما كسبتموه. اطلبوا الاختبارات على دفعات صغيرة بما يكفي لمراجعتها. اقرؤوا كل اختبار جديد، وتفحّصوا بعناية أي تغيير في اختبار تعتمدون عليه بالفعل.
هناك طريقة معيّنة تفسد بها مجموعة الاختبارات المولَّدة، وهي تستحق شريحة خاصة بها. الكود الحقيقي يتعامل مع أشياء خارجه، مثل قاعدة بيانات أو ساعة أو خدمة دفع.
ويمكن للاختبارات أن تستبدل هذه الاعتماديات الخارجية بـ mocks. الـ mock كائن بديل يمرّره الاختبار مكان الكائن الحقيقي، ويجيب بالطريقة التي يحدّدها له الاختبار. فخدمة دفع مزيّفة تتيح لاختبار وحدة أن يفحص معالجة الدفع دون إجراء دفعة حقيقية. هذا mock مفيد.
وإليكم الحالة الضارة. يُفترض أن يفحص هذا الاختبار أن مجموع الشراء يشمل الضريبة. لكنه يستبدل حساب الضريبة الخاص بنا بـ mock، ويطلب من الـ mock أن يُرجع 118.
ثم يتحقّق الـ assertion من أن النتيجة 118، وهو الرقم الذي ضبطه الاختبار بنفسه. حساب الضريبة الحقيقي لا يُنفَّذ أبدًا، لذا سينجح الاختبار حتى لو كان هذا الحساب خاطئًا أو محذوفًا.
والآن لنرَ كيف نقيّم الاختبارات نفسها. يمكنكم الوثوق باختبار بعد أن تروه يفشل للسبب الصحيح. في التطوير الموجَّه بالاختبارات (TDD) هذه خطوة الأحمر: يُكتب الاختبار أولًا، ويفشل قبل أن يوجد الكود. وعندما تُكتب الاختبارات بعد الكود، كما يكتبها الوكيل غالبًا، ما زال بإمكانكم الحصول على هذا الدليل. احذفوا عمدًا القاعدة التي يجب أن يلتقطها الاختبار، وشغّلوا مجموعة الاختبارات. هذه الدالة تقرّر هل يمكن إضافة عنصر جديد إلى قائمة المهام. السطر المميَّز يرفض العنصر إذا كان فارغًا بعد حذف المسافات من طرفيه. قبل أن نلمس أي شيء، تنجح الاختبارات الـ 42 كلها.
احذفوا هذا السطر مؤقتًا. صار البرنامج الآن يقبل عنصرًا فارغًا، لذا يجب أن يفشل أي اختبار يغطي هذه الحالة.
في هذا التشغيل التوضيحي، ما زالت الاختبارات الـ 42 كلها تنجح. لم يحاول أي منها إضافة عنصر فارغ، لذا لم تكتشف مجموعة الاختبارات الخلل الذي أدخلناه عمدًا.
لذا أضيفوا الحالة الناقصة. يحاول هذا الاختبار إضافة عنصر مكوَّن من مسافات فقط إلى قائمة فارغة، ويتوقّع أن تكون الإجابة False. ومع بقاء القاعدة محذوفة، يفشل الاختبار الجديد. وهذا الفشل هو بالضبط ما أردنا رؤيته.
والآن أعيدوا القاعدة وشغّلوا الاختبارات مجددًا. تنجح الاختبارات الـ 43 كلها. الاختبار الجديد يفشل من دون القاعدة وينجح معها، لذا نعرف أنه يفحص القاعدة. وهذا يخبركم بشيء لا تستطيع نسبة التغطية أن تخبركم به. هذه خطوة الأحمر، ولكن بأثر رجعي. عندما يكتب الوكيل الاختبارات بعد الكود، اطلبوا منه أن يريكم كل اختبار جديد يفشل عند حذف القاعدة، وينجح عند إعادتها.
فعل ذلك يدويًا لكل سطر سيكون بطيئًا. يمكن لأداة أن تُجري هذه التغييرات المتعمَّدة نيابةً عنكم. فهي تغيّر الكود تغييرًا صغيرًا في كل مرة، وتشغّل الاختبارات على كل نسخة معدَّلة من البرنامج. كل تغيير يُسمّى طفرة (mutation)، والتقنية كلها تُسمّى اختبار الطفرات (mutation testing). في Python، تقوم بذلك أداة اسمها mutmut. وفي JavaScript و TypeScript هناك Stryker.
الطفرة التي تكشفها الاختبارات تُسمّى killed. والتي تمرّ دون أن تُكشف تُسمّى survived. الطفرة الناجية قد تكشف ثغرة في الاختبارات، مع أن بعض الطفرات لا تغيّر السلوك أصلًا. افحصوا ما الذي تغيّر قبل أن تقرّروا أن هناك حاجة إلى اختبار جديد. عمليًا، اطلبوا من الوكيل أن يشغّل الأداة على الكود الذي غيّره للتو، وأن يحضر لكم الطفرات الناجية. ثم تقرّرون لكل واحدة منها هل ينقص اختبار.
تصف ورقة نشرتها Google عام 2021 اختبار الطفرات كما استخدمه أكثر من 24,000 مطوّر في أكثر من 1,000 مشروع.
تشغيل كل طفرة ممكنة على هذا النطاق مكلف أكثر من اللازم. لذا يركّز نظام Google على الكود المتغيّر ويصفّي الطفرات، فيرى المطوّرون عددًا صغيرًا منها يُرجَّح أن يكشف اختبارات ناقصة مفيدة.
عندما يكتب الوكيل الكود والاختبارات من فهم واحد لطلبكم، فقد يتسرّب أي سوء فهم فيه إلى كليهما. فتتفق الاختبارات مع الكود، وتبقى الاختبارات ناجحة مع أن الميزة معطّلة. هنا يقول المتطلب إن المجموع يشمل ضريبة 18%، والدالة نسيتها. والاختبار يحسب إجابته المتوقّعة بالحساب نفسه الذي تستخدمه الدالة. هذا اختبار تحصيل حاصل (tautological).
الإجابة المتوقّعة تُسقط الضريبة تمامًا كما تُسقطها الدالة. هذا الاختبار ينجح مع وجود الخطأ، وسيفشل لو صحّحتم الدالة لتشمل الضريبة.
أما التوقّع المستقل فيأتي من المتطلب نفسه. عنصران بسعر 50 لكل منهما يساويان 100، وضريبة 18% تجعل المجموع 118. الاختبار الذي يتحقّق من أن النتيجة 118 يفشل على الكود الحالي، ويشير مباشرة إلى الضريبة الناقصة.
وقد يَعِد اسم الاختبار بسلوك لا يفحصه الـ assertion فيه. اسم هذا الاختبار يوحي بأن مسح العناصر المكتملة يزيلها. يبني قائمة فيها عنصر مكتمل وعنصر مفتوح، ثم يمسح العناصر المكتملة. لكن الـ assertion فيه يتحقّق فقط من أن نتيجة ما قد عادت. وسينجح حتى لو لم يُحذف أي شيء.
قارنوا الـ assertion بالسلوك الذي يحمله اسم الاختبار. وهذا يعني هنا التحقّق من أن Call mom وحده يبقى بعد المسح. اقرؤوا الحالة الابتدائية والإجراء والنتيجة المتوقّعة لتروا هل يغطي الاختبار السلوك الذي يدّعي أنه يغطيه.
تغطية الأسطر تخبركم أيّ الأسطر القابلة للتنفيذ نُفِّذت أثناء تشغيل الاختبارات. إنها مقياس لما نُفِّذ من الكود، لا لما فُحص منه. في اتجاه واحد، التغطية موثوقة. إذا لم يشغّل أي اختبار سطرًا ما، أو لم يستدعِ دالة ما، فهذا الكود لم يُختبر أصلًا، ومن المفيد إضافة اختبارات تصل إليه.
هذا الاختبار يستدعي initials مع الاسم Alan Turing ويطبع ما تُرجعه، دون أي assertion. كل سطر في الدالة initials يُنفَّذ، لذا تغطية أسطرها 100%.
والآن افترضوا أن initials أعادت a t بأحرف صغيرة بدل A T بأحرف كبيرة. التغطية ما زالت 100%، والاختبار ما زال ينجح، إذ لا شيء يقارن النتيجة بقيمة متوقّعة.
التغطية المنخفضة تدلّ على الكود غير المختبَر. والعالية تُظهر أن أسطرًا كثيرة نُفِّذت، لكن ما زال عليكم فحص ما تحقّقت منه الاختبارات. استخدموا التغطية لتوجيه هذا الفحص.
لستم مضطرين إلى عدّ الأسطر بأنفسكم. في Python، توجد إضافة (plugin) اسمها pytest-cov تقيس التغطية بينما يشغّل pytest الاختبارات. أضيفوها إلى المشروع كاعتمادية تطوير. ثم شغّلوا pytest مع خيارين إضافيين. الأول يفعّل التغطية للمشروع. والثاني يطلب تقريرًا يسرد الأسطر غير المغطّاة.
في التقرير صفّ واحد لكل ملف. يبيّن عدد العبارات في الملف، وعدد ما لم يُنفَّذ منها، ونسبة ما نُفِّذ. العمود الأخير هو الأكثر فائدة. فهو يسمّي الأسطر التي لم يشغّلها أي اختبار. هنا، الأسطر من 42 إلى 47 في الوحدة todo لم تُنفَّذ قط.
ابدؤوا البحث من هذه الأسطر غير المغطّاة. يمكنكم أن تطلبوا من الوكيل تشغيل التقرير وشرح الثغرات، لكن اقرؤوا التقرير بأنفسكم أيضًا. أما الأسطر التي نُفِّذت، فارجعوا فيها إلى الـ assertions. فكما رأينا للتو، السطر الذي نُفِّذ لم يُفحَص بالضرورة.
حذف اختبار أو تحسينه قرار صيانة عادي. إليكم أربعة اختبارات للدالة initials. لنمرّ عليها واحدًا تلو الآخر ونقرّر هل نُبقي كلًّا منها، أم نحسّنه أم نحذفه.
الاختباران الأولان يفحصان النوع نفسه من الحالات: اسم أول يليه اسم العائلة. الثاني لا يضيف حالة مختلفة، وسيفشل للأسباب نفسها، لذا يمكن حذفه.
الثالث يستدعي initials أيضًا، لكن مع اسم فارغ. هذه حالة مختلفة لها نتيجتها المتوقّعة الخاصة، لذا يبقى، مع أنه يستدعي الدالة نفسها.
الرابع يشغّل initials ولا يفحص شيئًا. يزيد التغطية، لكنه لن يفشل حتى لو كانت الإجابة خاطئة.
حسّنوه بإضافة قيمة متوقّعة. الآن ستجعله الإجابة الخاطئة يفشل.
أبقوا على الاختبار الذي يفحص حالة مختلفة تهمّكم. وحسّنوا الاختبار الذي لا يفحص السلوك المقصود. واحذفوا ما يكرّر اختبارًا آخر. وفي مجموعة اختبارات مولَّدة، أبقوا على الاختبارات التي تفهمون توقّعاتها، ويخبركم فشلها بشيء مفيد.
قيمة مجموعة الاختبارات فيما تكشفه. أبقوا على الاختبارات التي تكشف الأخطاء التي تهمّكم، ولا تتردّدوا في التخلّي عن البقية. شكرًا على المشاهدة، ونراكم في الفيديو التالي.