لماذا تنتمي اختبارات الوحدة إلى مقدمة خط أنابيبك
تخيل أنك تدفع تغييرًا في الكود بعد ظهر يوم الجمعة. ينجح البناء، ويتم النشر، وتعود إلى المنزل. صباح السبت، يضيء هاتفك بالتنبيهات. عملية حساب خصم تطبق قيمًا سالبة على طلبات العملاء. بدا المنطق صحيحًا أثناء المراجعة. لكن لم يكتشف أحد الحالة الحدودية حيث يؤدي دمج كوبون خصم مع سعر بيع إلى إنتاج إجمالي سالب.
هذا هو النوع من المشاكل التي توجد اختبارات الوحدة لاكتشافها. ليس لأنها متطورة، ولكن لأنها تعمل بسرعة، وتعمل مبكرًا، وتعمل في عزلة. إنها خط الدفاع الأول في أي خط أنابيب تسليم.
ما تتحقق منه اختبارات الوحدة فعليًا
يتحقق اختبار الوحدة من سلوك واحد ذي معنى من نقطة الدخول التي يبدأ عندها هذا السلوك. في خدمة خلفية، قد تكون نقطة الدخول هذه نقطة نهاية REST أو حالة استخدام. يُسمح للطلب بالمرور عبر الطبقات الداخلية الحقيقية: المتحكم، الخدمة، منطق المجال، وحدود المستودع. لا يحاول الاختبار إثبات أن طريقة واحدة تستدعي طريقة أخرى. إنه يحاول إثبات أن النظام يستجيب بشكل صحيح لمدخل ذي معنى.
إذا كان لديك دالة تحسب تكلفة الشحن بناءً على الوزن والوجهة، يمكن لاختبار الوحدة تأكيد:
- الوزن القياسي يعيد التكلفة القياسية
- الوزن الصفري يعيد تكلفة صفرية
- الوزن السالب يعيد خطأ أو صفرًا
- الوزن الأقصى يعيد قيمة الحد الأقصى
ما لا يجب أن يحاول اختبار الوحدة إثباته هو ما إذا كانت قاعدة البيانات الحقيقية تخزن تلك التكلفة بشكل صحيح، أو ما إذا كانت بوابة الدفع الحقيقية تقبلها، أو ما إذا كانت الخدمة المجاورة متاحة حقًا. هذه مخاوف لأنواع اختبار أخرى.
قيمة اختبارات الوحدة ضيقة ولكنها عميقة. إنها تمنحك الثقة بأن سلوكًا معينًا لا يزال يعمل عندما يكون النظام المحيط مسيطرًا عليه. عندما تغير الكود لاحقًا، تخبرك اختبارات الوحدة الناجحة أنك لم تكسر السلوك الذي تحققت منه بالفعل.
مبدأ العزلة
لكي تكون اختبارات الوحدة سريعة وموثوقة، يجب التحكم في العالم المحيط. يجب أن تعمل طبقات التطبيق الداخلية بشكل طبيعي. يجب ألا تقرر الجيران الخارجيون نتيجة الاختبار. هذا يعني عادةً عدم وجود اتصال حقيقي بقاعدة بيانات الإنتاج، ولا استدعاء HTTP مباشر لطرف ثالث، ولا اعتماد على خدمة أخرى قيد التشغيل. إذا كان السلوك يحتاج إلى بيانات، يمكن للاختبار استخدام بيانات مسيطر عليها، أو قاعدة بيانات اختبار محلية/في الذاكرة، أو وهمية، أو محاكاة، أو كعب عند حدود النظام.
لهذا السبب لا ينبغي تعريف اختبار الوحدة على أنه "اختبار واحد لكل طريقة" أو "اختبار واحد لكل كلاس". هذا التعريف ميكانيكي جدًا وغالبًا ما يدفع الفرق إلى اختبار تفاصيل التنفيذ. من الأفضل فهم اختبار الوحدة على أنه اختبار سلوك من نقطة دخول ذات صلة، مع التحكم في العالم الخارجي بما يكفي ليشير الفشل إلى السلوك الذي يتم اختباره.
الكود المحمول (Mobile code) هو مثال مفيد. بعض السلوكيات تكون منطقية فقط داخل بيئة تشغيل محمولة. في هذه الحالة، استخدام محاكي لا يجعل الاختبار خاطئًا تلقائيًا. السؤال هو ما إذا كان الاختبار لا يزال يركز على سلوك واحد ويتحكم في التبعيات المحيطة به. إذا كان الأمر كذلك، فلا يزال بإمكانه خدمة غرض اختبار الوحدة في خط الأنابيب.
هذه العزلة هي ما يجعل اختبارات الوحدة سريعة. تنتهي مجموعة اختبارات الوحدة المكتوبة جيدًا لخدمة خلفية نموذجية في ثوانٍ، وليس دقائق. قارن ذلك باختبارات التكامل التي تشغل الحاويات أو تتصل بقواعد بيانات الاختبار. تلك تستغرق دقائق.
السرعة مهمة لأن الاختبارات السريعة يتم تشغيلها كثيرًا. يقوم المطورون بتشغيلها محليًا قبل دفع الكود. تقوم خطوط أنابيب CI بتشغيلها فورًا بعد البناء. إذا حدث كسر، فستعرف في غضون ثوانٍ أو دقائق، وليس بعد انتظار مجموعة اختبار كاملة تستغرق نصف ساعة.
أين تتناسب اختبارات الوحدة في خط الأنابيب
تنتمي اختبارات الوحدة إلى المرحلة الأولى من خط الأنابيب الخاص بك، مباشرة بعد تجميع الكود أو بنائه. المنطق بسيط: إذا كان السلوك الأساسي الذي يعرضه التطبيق معطلاً بالفعل، فلا فائدة من تشغيل اختبارات أبطأ تعتمد على أنظمة مجاورة حقيقية.
يبدو ترتيب مراحل خط الأنابيب النموذجي كما يلي:
إليك مثال عملي لكيفية ظهور تلك الخطوة الأولى في ملف تكوين CI:
# .gitlab-ci.yml or similar CI config
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
test-unit:
stage: test
script:
- npm test -- --coverage
only:
- merge_requests
- main
- بناء أو تجميع الكود
- تشغيل اختبارات الوحدة
- تشغيل التحليل الثابت أو الفحص (linting)
- بناء صور الحاويات أو القطع الأثرية
- تشغيل اختبارات التكامل
- النشر إلى بيئة التدريج (staging)
- تشغيل اختبارات النهاية إلى النهاية أو اختبارات القبول
- النشر إلى الإنتاج
إذا فشلت اختبارات الوحدة في الخطوة 2، يتوقف خط الأنابيب. لا يتم بناء أي حاويات. لا يتم شغل أي بيئة تدريج. لا يضيع أي وقت في انتظار اختبارات التكامل التي ستفشل على أي حال لأن المنطق الأساسي خاطئ.
هذه هي حلقة التغذية الراجعة السريعة. كلما اكتشفت خطأ مبكرًا، كان إصلاحه أرخص. الخطأ الذي يتم العثور عليه أثناء اختبار الوحدة يكلف دقائق لإصلاحه. الخطأ الذي يتم العثور عليه في الإنتاج يكلف الاستجابة للحوادث، وإجراءات التراجع، والتواصل مع العملاء، وربما إصلاح البيانات.
عندما لا تكون اختبارات الوحدة كافية
اختبارات الوحدة لها نقطة عمياء: لا يمكنها التحقق من أن المكونات تعمل معًا في النظام الحقيقي. إذا كان سلوك الدفع الخاص بك يعتمد على واجهة برمجة تطبيقات دفع خارجية، يمكن لاختبار الوحدة التحقق من كيفية تصرف الكود الخاص بك عندما تعيد تبعية الدفع نجاحًا، أو فشلًا، أو انتهاء مهلة، أو بيانات مشوهة. لا يمكنه إخبارك ما إذا كانت واجهة برمجة تطبيقات الدفع الحقيقية تقبل تنسيق طلبك، أو تتعامل مع المصادقة، أو تعيد بنية الاستجابة المتوقعة.
لهذا، تحتاج إلى اختبارات التكامل. لكن اختبارات الوحدة لا تزال تخدم غرضًا هنا. إنها تتحقق من أن هيكل الكود الخاص بك صحيح، وأن المعاملات تمر بالترتيب الصحيح، وأن معالجة الأخطاء تعمل كما هو متوقع. لكنها لا يمكنها استبدال فحص التكامل الحقيقي.
قيد آخر هو أن اختبارات الوحدة لا يمكنها اكتشاف مشاكل التكوين، أو اختلافات البيئة، أو مشاكل البنية التحتية. قد تفشل دالة تعمل بشكل مثالي في اختبارات الوحدة في الإنتاج لأن قاعدة بيانات الإنتاج لديها إعداد ترتيب (collation) مختلف، أو لأن متغير بيئة مطلوب مفقود. تتطلب هذه المشاكل طرق اختبار مختلفة.
ما هو القدر الكافي من اختبارات الوحدة
تعتمد الإجابة على المخاطر. إذا كانت الدالة تنفذ منطق أعمال أساسي حيث يمكن أن يتسبب الخطأ في خسارة مالية، أو تلف بيانات، أو مشاكل سلامة، فأنت تريد اختبارات وحدة شاملة تغطي الحالات العادية، والحالات الحدودية، وحالات الخطأ، والظروف الحدودية.
إذا كانت الدالة ببساطة تمرر البيانات من مكان إلى آخر دون تحويل، فقد يكون اختبار وحدة واحد يؤكد أن التمرير يعمل بشكل صحيح كافيًا. قضاء ساعات في كتابة اختبارات شاملة لكود تافه ليس استخدامًا جيدًا للوقت.
النهج العملي هو الاختبار القائم على المخاطر. حدد أي أجزاء من قاعدة الكود الخاصة بك تحمل أعلى مخاطرة إذا فشلت. ركز جهود اختبار الوحدة هناك. بالنسبة للكود منخفض المخاطر، اكتب اختبارات كافية فقط لاكتشاف الأخطاء الواضحة.
قائمة مراجعة عملية لاختبارات الوحدة في خط الأنابيب الخاص بك
- يتم تشغيل اختبارات الوحدة قبل أي اختبارات تكامل أو اختبارات نهاية إلى نهاية
- تكتمل اختبارات الوحدة في غضون بضع دقائق لقاعدة الكود بأكملها
- لا تتطلب اختبارات الوحدة خدمات خارجية أو قواعد بيانات أو وصول إلى الشبكة
- يتحقق كل اختبار وحدة من سلوك واحد ذي معنى من نقطة دخول ذات صلة، وليس طريقة أو كلاس واحد
- الاختبارات حتمية: نفس المدخلات تنتج دائمًا نفس النتيجة
- توقف اختبارات الوحدة الفاشلة خط الأنابيب على الفور
- يمكن للمطورين تشغيل نفس الاختبارات محليًا قبل الدفع
الخلاصة الملموسة
لا تتعلق اختبارات الوحدة بتحقيق أرقام تغطية مثالية. إنها تتعلق باكتشاف الفئة الأكثر شيوعًا من الأخطاء في أقرب وقت ممكن، بأقل قدر من الوقت وتكلفة البنية التحتية. ضعها أولاً في خط الأنابيب الخاص بك، وحافظ على سرعتها، وحافظ على عزلتها، وركزها على المنطق الأكثر أهمية. كل شيء آخر يُبنى على هذا الأساس.