تطوير البرمجيات الموجه بالاختبار (TDD): استراتيجية كتابة اختبار واحد فقط

دقائق القراءة: 25

مقدمة في تطوير البرمجيات الموجه بالاختبار (TDD)

يُعرف Test Driven Development (TDD) أحيانًا بأنه منهجية "كتابة الاختبارات أولاً". ينص مبدأ TDD الأساسي على أنه لا ينبغي لنا كتابة أي كود برمجي قبل أن نكتب اختبارات آلية تقوم بتشغيل هذا الكود والتحقق منه. يُعدّ كتابة الكود أولاً سلوكًا غير مثالي، وهو بالطبع النمط المتبع في تطوير البرمجيات وفقًا لنموذج الشلال (waterfall model). في هذا النموذج، تُقسّم أنشطة تطوير البرمجيات إلى مراحل متسلسلة، مثل مرحلة جمع المتطلبات، ثم بناء التطبيق، ثم اختباره، ثم نشره، وهكذا.

منهجية Agile مقابل منهجية Waterfall: الفروقات الجوهرية

قد يتساءل البعض: ما الفرق بين هذا وبين منهجية Agile؟ ألا توجد لدينا نفس المراحل تمامًا في Agile؟ بالطبع توجد، لكن الفرق الجوهري يكمن في أن هذه المراحل في Agile ليست مقيدة ببوابات صارمة (gated). في نموذج الشلال (waterfall)، تُقيد المراحل وتُنفذ بتسلسل صارم. هذا يعني أننا لن نبدأ في بناء التطبيق حتى يتم جمع المتطلبات بالكامل، والتوقيع عليها، وتجميدها. بمجرد تجميد المتطلبات (والتحكم فيها بواسطة سياسات إدارة التغيير)، ننتقل إلى المرحلة التالية – بناء التطبيق.

وبالمثل، لن ننتقل إلى مرحلة الاختبار حتى يتم بناء التطبيق بالكامل ونصل إلى مرحلة "اكتمال الكود" (code complete milestone)، حيث يتم تجميد تغييرات الكود. بمجرد تجميد الكود (والتحكم فيه بواسطة سياسات إدارة التغيير)، نسلمه للمختبرين. تبدأ مرحلة الاختبار، وفقط بعد اكتمال جميع الاختبارات (وبشرط عدم اكتشاف عيوب كبيرة)، ننتقل إلى مرحلة النشر.

في منهجية Agile، نقوم بجميع الأنشطة المذكورة أعلاه بالتوازي، وفي نفس الوقت. نستمر في العمل على قصص المستخدم (user stories) (المواصفات) بينما نبني التطبيق في نفس الوقت. أثناء بناء التطبيق، نقوم أيضًا باختباره. وأثناء بناء واختبار التطبيق، نقوم أيضًا بنشره. نتعلم من التطبيق المنشور في بيئة الإنتاج ونستخدم هذا التعلم المؤكد كتعليقات تغذي قصص مستخدم جديدة. بهذه الطريقة، تُغلق الحلقة، ونحن نعمل بشكل تكراري، ونحسن القيمة تدريجيًا. الطريقة الوحيدة لتمكين تدفق القيمة التكراري هذا هي الاعتماد على الاختبارات الآلية، وكما وصفنا، تُكتب هذه الاختبارات في وقت مبكر جدًا من عملية التطوير. في الواقع، يجب كتابة الاختبارات قبل كتابة كود التطبيق الفعلي.

لماذا لا تكتب جميع اختباراتك أولاً؟ فقط اكتب اختباراً واحداً!

قد يبدو عنوان هذا المقال "لا تكتب جميع اختباراتك أولاً، فقط اكتب اختبارًا واحدًا" مربكًا بعض الشيء. دعونا نفكك معنى هذا العنوان ونستكشف الفكرة الكامنة وراءه. ولكن أولاً، إليك نظرة عامة على التقنيات التي سنستخدمها في مثالنا العملي.

حزمة التقنيات المستخدمة في هذا المثال العملي

للحفاظ على بساطة المثال وسهولة متابعته، اخترت منصة .NET Core جنبًا إلى جنب مع منصة الاختبار xUnit.net. لمتابعة أمثلة الكود، يرجى تثبيت .NET Core و xUnit.net. لتتمكن من تشغيل الكود النموذجي، يرجى فتح ملف ./tests/tests.csproj وإضافة هذا السطر إلى قسم ItemGroup:

<ProjectReference Include= "../app/app.csproj" />

أنت الآن جاهز تمامًا لمتابعة التمارين البرمجية.

مثال بسيط: حاسبة الإكرامية (Tip Calculator)

لفهم الفرق بين كتابة جميع الاختبارات أولاً وكتابة اختبار واحد فقط أولاً، قد يكون من الأفضل أن نوضح ذلك بالمثال بدلاً من مجرد الشرح. لذا، دعونا نحاول بناء مثال بسيط – لهذه التجربة، اخترت حالة تافهة لحساب الإكرامية في مطعم. غالبًا ما نجد أنفسنا في موقف نرغب فيه في إكرامية المطعم مقابل الخدمة، ولكن من الصعب حساب النسب المئوية في أذهاننا. لذا، قد تكون "حاسبة إكرامية" (Tip Calculator) صغيرة مفيدة. إليك التوقعات:

  • بصفتي زبونًا: أرغب في حساب الفاتورة الإجمالية (المجموع بالإضافة إلى الإكرامية).
  • لأنني: أرغب في مكافأة المطعم على الخدمة.

سيناريوهات الاستخدام لحاسبة الإكرامية

  1. السيناريو رقم 1: الزبون يحسب الإجمالي لخدمة "سيئة للغاية" (terrible service).
    • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
    • و: كانت الخدمة سيئة للغاية.
    • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
    • فإن: حاسبة الإكرامية تعرض 100.00 دولار كرسوم إجمالية.
  2. السيناريو رقم 2: الزبون يحسب الإجمالي لخدمة "سيئة" (poor service).
    • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
    • و: كانت الخدمة سيئة.
    • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
    • فإن: حاسبة الإكرامية تعرض 105.00 دولار كرسوم إجمالية.
  3. السيناريو رقم 3: الزبون يحسب الإجمالي لخدمة "جيدة" (good service).
    • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
    • و: كانت الخدمة جيدة.
    • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
    • فإن: حاسبة الإكرامية تعرض 110.00 دولار كرسوم إجمالية.
  4. السيناريو رقم 4: الزبون يحسب الإجمالي لخدمة "ممتازة" (great service).
    • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
    • و: كانت الخدمة ممتازة.
    • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
    • فإن: حاسبة الإكرامية تعرض 115.00 دولار كرسوم إجمالية.
  5. السيناريو رقم 5: الزبون يحسب الإجمالي لخدمة "رائعة" (excellent service).
    • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
    • و: كانت الخدمة رائعة.
    • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
    • فإن: حاسبة الإكرامية تعرض 120.00 دولار كرسوم إجمالية.

دعونا الآن نطبق قصة المستخدم المذكورة أعلاه. نرى أن القصة تحتوي على 5 معايير قبول (أو سيناريوهات). ننتقل الآن إلى مرحلة التحليل – نفكر في الوظيفة الأولى التي يجب أن يطبقها تطبيق "حاسبة الإكرامية" (Tip Calculator) الخاص بنا. ولكن أولاً، دعنا نفتح نافذة الأوامر وننشئ الدليل الجديد:

md TipCalculator
cd TipCalculator

ثم ننشئ دليلي app و tests داخل دليل TipCalculator. الآن، انتقل إلى cd tests وقم بتشغيل:

dotnet new xunit

ثم cd .. وانتقل إلى cd app، ثم قم بتشغيل:

dotnet new classlib

نحن الآن جاهزون للبدء! افتح محرر النصوص المفضل لديك (محرري المفضل هو Visual Studio Code) وركز ذهنك على التوقعات. ما السلوك الذي نتوقعه من "حاسبة الإكرامية" (Tip Calculator)؟ لتضييق نطاق توقعاتنا، غالبًا ما يساعدنا أخذ معيار قبول واحد (أي سيناريو واحد) والتركيز عليه أولاً. دعنا نأخذ السيناريو رقم 1:

السيناريو رقم 1: الزبون يحسب الإجمالي لخدمة "سيئة للغاية" (terrible service).

  • بالنظر إلى: أن إجمالي فاتورة المطعم هو 100.00 دولار.
  • و: كانت الخدمة سيئة للغاية.
  • عندما: تحسب حاسبة الإكرامية الرسوم الإجمالية.
  • فإن: حاسبة الإكرامية تعرض 100.00 دولار كرسوم إجمالية.

في حالة كانت الخدمة سيئة للغاية، لا نضيف أي إكراميات، و"حاسبة الإكرامية" (Tip Calculator) تحسب إكرامية بقيمة 0.00 دولار. فكيف نقوم بأتمتة هذا السيناريو؟ توقعي الأول هو أننا بحاجة بطريقة ما لإبلاغ "حاسبة الإكرامية" (Tip Calculator) بأن الخدمة كانت سيئة للغاية. إما أن نكتب كلمة 'Terrible' في حقل الإدخال، أو نختار 'Terrible' من قائمة تقييمات الخدمة المتاحة. لذا، فإن أول شيء نفعله هنا هو صياغة بعض التوقعات فيما يتعلق بقدرة "حاسبة الإكرامية" (Tip Calculator) على تلقي إشعار بأن الخدمة كانت سيئة للغاية. أحب دائمًا أن أبدأ بتوقع أن ما يدخله المستخدم صالح. لذا سأكتب أولاً اختبارًا يتحقق مما إذا كان التقييم 'Terrible' معترفًا به بواسطة "حاسبة الإكرامية" (Tip Calculator) كتقييم صالح.

انتقل إلى دليل tests، أعد تسمية ملف UnitTest1.cs الذي تم إنشاؤه تلقائيًا إلى TipCalculatorTests.cs وأضف الاختبار التالي:

[ Fact ]
public void CheckIfRatingTerribleIsValid ( )
{
    var expectedResponseForValidRating = true ;
    var actualResponseForValidRating = false ;
    Assert.Equal(expectedResponseForValidRating, actualResponseForValidRating);
}

الآن انتقل إلى سطر الأوامر، cd tests، وقم بتشغيل:

dotnet test

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل الاختبار الأولي

بالطبع، سيفشل الاختبار التافه المذكور أعلاه، لأننا قمنا بتشفير القيم (hardcoded). ولكن من الممارسات الجيدة دائمًا التأكد من أننا نرى اختباراتنا تفشل قبل المتابعة. عدم ملاحظة فشل الاختبار قد يعطينا إحساسًا زائفًا بالأمان لاحقًا، إذا لم تفشل أي اختبارات وانتهى بنا المطاف إلى الاعتقاد بأن كل شيء يعمل كما هو متوقع.

بعض الملاحظات الإضافية حول الاختبار أعلاه:

  • يساعد أن يكون اسم الاختبار وصفيًا. اخترت CheckIfRatingTerribleIsValid للتعبير عن حقيقة أنه يجب علينا التأكد من أن تطبيقنا قادر على التعرف على أوامرنا.
  • يساعد أيضًا أن تكون أسماء المتغيرات المتوقعة والفعلية وصفية. اخترت expectedResponseForValidRating و actualResponseForValidRating كدلالة واضحة على ما نتوقعه في هذا الاختبار، وأيضًا ما هي القيمة الفعلية التي ستنتجها "حاسبة الإكرامية" (Tip Calculator).
  • الاختبار هو كود مصدري من الدرجة الأولى (first-class source code) ويجب التعامل معه بنفس العناية التي تُمنح لكود الإنتاج.

أول قرار تصميمي: كيفية تخزين تقييمات الخدمة

في هذه المرحلة، نضطر لاتخاذ قرار – كيف ستعرف "حاسبة الإكرامية" (Tip Calculator) الناشئة ما إذا كان تقييم الخدمة المقدم من قبل المستخدم صالحًا أم لا؟ القرار التصميمي الذي يتبادر إلى الذهن هو أن "حاسبة الإكرامية" (Tip Calculator) يجب أن تكون قادرة على تخزين واسترجاع بعض البيانات. في هذه الحالة، البيانات التي نهتم بها هي تقييم الخدمة.

إذا عدنا إلى قصة المستخدم وراجعنا معايير القبول الخمسة، فسنرى أن التوقعات هي أن "حاسبة الإكرامية" (Tip Calculator) يجب أن تكون قادرة على التعرف على خمسة تقييمات خدمة مختلفة:

  • Terrible (سيئة للغاية)
  • Poor (سيئة)
  • Good (جيدة)
  • Great (ممتازة)
  • Excellent (رائعة)

لذا، فإن أبسط طريقة لجعل "حاسبة الإكرامية" (Tip Calculator) تخزن هذه المعلومات هي تزويدها بمصفوفة (array) أو قائمة (list). ولكن بدلاً من التسرع في تنفيذ هذه القائمة، يجب علينا فحص التوقعات مرة أخرى، لمعرفة ما إذا كان هناك أي شيء آخر قد فاتنا. وبالفعل هناك – ليس فقط يجب أن تكون "حاسبة الإكرامية" (Tip Calculator) قادرة على التعرف على تقييمات الخدمة الصالحة، بل يجب أن تكون قادرة أيضًا على ربط كل تقييم بقيمة نسبة مئوية. يظهر تحليلنا الارتباطات التالية:

  • Terrible => 0%
  • Poor => 5%
  • Good => 10%
  • Great => 15%
  • Excellent => 20%

في هذه الحالة، لن تكون مصفوفة بسيطة أو قائمة بسيطة كافية لاحتواء الارتباطات المذكورة أعلاه. ما هو أبسط هيكل بيانات تالي سيسمح لنا بتطبيق هذه الارتباطات؟ بعد إجراء القليل من البحث، نكتشف أن Hashtable هو على الأرجح هيكل البيانات الأكثر ملاءمة الذي يمكن أن يغطي احتياجاتنا بأقل قدر من التعقيد.

نتنقل الآن إلى دليل app ونعيد تسمية ملف Class1.cs الذي تم إنشاؤه تلقائيًا إلى TipCalculator.cs. نريد الآن إضافة Hashtable ستحتوي على تقييمات الخدمة وقيم النسب المئوية المرتبطة بها:

System.Collections.Hashtable ratingPercentages = new System.Collections.Hashtable();

الآن هو الوقت المناسب لتذكر أن TDD يركز على ربط التوقعات بسلوك التطبيق، وليس بهيكل التطبيق. بمعرفة ذلك، نحتاج إلى تعديل اختبارنا لجعل "حاسبة الإكرامية" (Tip Calculator) تُظهر بعض السلوك. يقوم الاختبار بتشفير بعض التوقعات فيما يتعلق بكيفية تصرف التطبيق، ويوفر التطبيق قيد التشغيل دليلًا على السلوك المتوقع. ولكن ما هو دليل سلوك التطبيق؟ لا توجد طريقة أخرى لتقييم سلوك التطبيق سوى من خلال فحص القيم التي ينتجها التطبيق قيد التشغيل. في هذه الحالة، نتوقع أن ينتج التطبيق قيم true أو false (قيم منطقية Boolean) بعد أن نسأل التطبيق عما إذا كانت قيمة معينة (أي تقييم الخدمة) صالحة.

لتدريب التطبيق على التصرف بالطريقة المتوقعة، نحتاج إلى تزويده بواجهة برمجة تطبيقات (API). في هذه الحالة، نصمم واجهة برمجة التطبيقات على النحو التالي:

 public bool CheckIfRatingIsValid ( string rating )

في اختبارنا، سنعدل القيمة المتوقعة الفعلية لتشغيل التطبيق قيد التشغيل وجمع قيمة الإخراج:

لقطة شاشة لمحرر الكود تظهر خطأ في عدم العثور على الدالة CheckIfRatingIsValid قبل تنفيذها

كما ترون من لقطة الشاشة أعلاه، قمنا بإنشاء كائن من TipCalculator ولكن عند محاولة الطلب من الكائن التحقق مما إذا كان التقييم المقدم ("Terrible") صالحًا، يشتكي المحرر من أنه لا يمكنه العثور على هذه الدالة (method). حسنًا بالطبع، لم يتم تنفيذ الدالة بعد. الآن حان الوقت للمضي قدمًا والقيام بذلك:

 public bool CheckIfRatingIsValid ( string rating )
{
    return false ;
}

الآن بعد أن تم تنفيذ الدالة، يعمل الاختبار؛ إليك القائمة الكاملة:

 using Xunit;
 using app;
 namespace tests
 {
    public class TipCalculatorTests
    {
        TipCalculator tipCalculator = new TipCalculator();

        [ Fact ]
        public void CheckIfRatingTerribleIsValid ( )
        {
            var expectedResponseForValidRating = true ;
            var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid( "Terrible" );
            Assert.Equal(expectedResponseForValidRating, actualResponseForValidRating);
        }
    }
 }

نرى من المثال أعلاه أننا نغش مرة أخرى (لقد قمنا بتشفير return false; في الدالة الجديدة التي أنشأناها). ما الفائدة من المماطلة وإنشاء هياكل عظمية وسقالات فقط بدلاً من تشمير سواعدنا والقيام بالبرمجة الفعلية؟ دعونا نناقش هذا الموضوع المهم.

مناقشة حول قرارنا التصميمي الأول: التكرار في TDD

نحن نوضح هنا كيفية القيام بمنهجية TDD خطوة بخطوة. الجزء المضحك هو أن هذا التوضيح خطوة بخطوة هو في الواقع الطريقة الدقيقة التي نتبعها في TDD: خطوة بخطوة. لا توجد طريقة أخرى للقيام بـ TDD إلا عن طريق القيام بذلك خطوة بخطوة، خطوة واحدة في كل مرة. كيف يختلف ذلك عن أي طريقة أخرى لتطوير البرمجيات؟ ألا نفعل كل شيء خطوة بخطوة حتى عندما لا نتبع منهجية TDD؟ حسنًا، ليس تمامًا. دعني أشرح:

يشعرني TDD وكأنني أركب حصانًا يركض بسرعة. نحن نتحرك بسرعة نحو هدفنا، لكننا نلامس الأرض بشكل متكرر (الحصان الذي يركض يضرب الأرض بين الحين والآخر ليرتد ويركض بسرعة). بالمقارنة، عندما أقوم بتطوير البرمجيات بدون TDD، أشعر وكأنني أطير طائرة ورقية. أقوم بحركات سريعة بالطائرة الورقية، لكنني لا ألمس الأرض أبدًا، ولا مرة واحدة. بحلول الوقت الذي تهبط فيه الطائرة الورقية، قد لا يكون مكان الهبوط هو المكان الذي قصدت أن تذهب إليه (من الصعب جدًا التحكم في اتجاه طائرة ورقية إذا كانت تطير في رياح قوية).

مع TDD، في أي وقت نجري فيه تغييرًا على الكود (كود الاختبار وكود التطبيق الفعلي)، نقوم بتشغيل الاختبارات وبالتالي نلامس الأرض. نحن نركض بسرعة، ولكن في نفس الوقت نحتاج إلى هذا التثبيت المتكرر. نحتاج إلى معرفة ما إذا كنا نسير في الاتجاه الصحيح وما إذا كنا قد كسرنا أي شيء أثناء ركضنا. اختباراتنا هي العراف الذي يخبرنا دائمًا ما إذا كان كل شيء يعمل كما هو متوقع أو إذا بدأ شيء ما في التصرف بشكل خاطئ. إن إجراء تغييرات على الكود عمل محفوف بالمخاطر. يوفر TDD نظامًا لطيفًا يوجه قراراتنا التصميمية ويضمن عدم إفساد شيء قمنا بالفعل بتأكيد أنه يعمل وفقًا لتوقعاتنا.

استبدال القيمة المشفرة بمنطق المعالجة الفعلي

دعنا الآن نستبدل القيمة المشفرة (hardcoded) بكود تشغيل فعلي. أولاً، نعلم "حاسبة الإكرامية" (Tip Calculator) أن هناك تقييم خدمة يسمى "Terrible" وأن نسبة الإكرامية المرتبطة بهذا التقييم هي 0:

 public bool CheckIfRatingIsValid ( string rating )
{
    ratingPercentages.Add( "Terrible" , 0 );
    return false ;
}

أصبحت "حاسبة الإكرامية" (Tip Calculator) الآن على دراية بوجود تقييم خدمة يسمى "Terrible" وأن نسبة الإكرامية المرتبطة بالخدمة السيئة للغاية هي 0%. رائع، لكننا ما زلنا نعيد القيمة المشفرة false. حان الوقت لاستبدالها بالحساب الفعلي:

 public bool CheckIfRatingIsValid ( string rating )
{
    ratingPercentages.Add( "Terrible" , 0 );
    return ratingPercentages.ContainsKey(rating);
}

شغل الاختبار مرة أخرى:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح الاختبار بعد إضافة منطق المعالجة

رائع، لكن الكود لا يزال يبدو مصطنعًا. نحن نقوم بتحميل قيمة "Terrible" في كائن Hashtable ratingPercentages ثم نتحقق فورًا مما إذا كانت هذه القيمة موجودة في Hashtable. الآن بعد أن انتقلنا من الاختبار الفاشل (Red) إلى الاختبار الناجح (Green)، حان الوقت لتنفيذ الخطوة الثالثة في حلقة TDD – إعادة الهيكلة (Refactor). إعادة الهيكلة هي في الأساس نشاط تعديل هيكل الكود دون التأثير على سلوك الكود. مهمتنا هنا بسيطة: استخراج الكود المسؤول عن ملء Hashtable ratingPercentages إلى كتلة كود منفصلة. المكان الأكثر طبيعية لهذا التحميل هو في كتلة الكود التي تقوم بتهيئة "حاسبة الإكرامية" (Tip Calculator) – دالة البناء (constructor method). بعد إعادة الهيكلة، يبدو كود مصدر تطبيقنا الفعلي كما يلي:

 using System.Collections;
 namespace app
 {
    public class TipCalculator
    {
        private Hashtable ratingPercentages = new Hashtable();

        public TipCalculator ( )
        {
            ratingPercentages.Add( "Terrible" , 0 );
        }

        public bool CheckIfRatingIsValid ( string rating )
        {
            return ratingPercentages.ContainsKey(rating);
        }
    }
 }

شغل الاختبار مرة أخرى، وينجح (نحن في المرحلة الخضراء green). لقد قمنا بتعديل هيكل الكود دون تعديل سلوكه! عمل جيد.

اختبار التوقعات السلبية: التحقق من القيم غير الصالحة

في أي وقت نلبي فيه توقعًا إيجابيًا، فمن الممارسات الحكيمة أن نقلب الأمور رأسًا على عقب ونصف التوقع السلبي. في هذه المرحلة، بما أننا تأكدنا من وجود قيمة تقييم خدمة مشروعة في "حاسبة الإكرامية" (Tip Calculator)، فإننا نريد التأكد من أن القيم غير المشروعة غير موجودة في "حاسبة الإكرامية" (Tip Calculator). ماذا نعني بالقيم غير المشروعة؟ أي قيمة بخلاف "Terrible" و "Poor" و "Good" و "Great" و "Excellent". حان الوقت لكتابة التوقع الجديد (أي الاختبار):

[ Fact ]
public void CheckIfRatingWhateverIsValid ( )
{
    var expectedResponseForValidRating = true ;
    var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid( "Whatever" );
    Assert.Equal(expectedResponseForValidRating, actualResponseForValidRating);
}

شغل الاختبارات:

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل الاختبار لقيمة تقييم غير صالحة

فشل. كما هو متوقع. (لقد حددنا أن توقعنا عند تقديم تقييم الخدمة كـ "Whatever" يجب أن يكون true. في الواقع، هو false، لأن "حاسبة الإكرامية" (Tip Calculator) الخاصة بنا لا تحتوي على قيمة "Whatever".) قم بإصلاح الاختبار (غيّر expectedResponseForValidRating من true إلى false) وشغله مرة أخرى:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح الاختبار بعد إصلاح التوقع السلبي

لحظة تأمل: لماذا قمنا بتزييف تشغيل الاختبار الأول وجعله يفشل؟ لأننا نريد دائمًا التأكد من أننا نلاحظ فشل اختبارنا الجديد. بهذه الطريقة، سنعرف أنه في المستقبل، أي نجاح للاختبار ليس مجرد إيجابية كاذبة (false positive).

في مدح الحالة المستقرة (Steady State)

هندسة البرمجيات هي عملية موازنة بين الحالة المستقرة (steady state) وفترات الحالة غير المستقرة (unstable state). ماذا نعني بالحالة المستقرة؟ إذا كان لدينا نظام (تطبيق قيد التشغيل) يتصرف بالطريقة التي نتوقعها (أي أنه ينتج قيمًا حددناها كقيم متوقعة)، فإننا نعلن أن النظام في حالة مستقرة. إنه يعمل ويقدم بعض القيمة. لا يزال تقديم القيمة جزئيًا. في حالتنا، القيمة الوحيدة التي تقدمها "حاسبة الإكرامية" (Tip Calculator) للمستخدمين هي قدرتها على التعرف على تقييم الخدمة "Terrible" كتقييم مشروع. بالإضافة إلى ذلك، فهي قادرة على إبلاغنا بأن تقييم الخدمة "Whatever" ليس تقييمًا مشروعًا. هذا ليس كثيرًا، لكنه لا يزال أفضل من لا شيء. والخبر السار – تطبيقنا قيد التشغيل حاليًا في حالة مستقرة.

الآن نريد أن ننظر في كيفية إضافة سلوك أكثر قيمة إلى "حاسبة الإكرامية" (Tip Calculator). والطريقة الوحيدة لإضافة المزيد من القيمة هي بإجراء بعض التغييرات. في أي وقت نجري فيه تغييرًا على تطبيقنا، فإننا نخل بحالته المستقرة. هذا الاضطراب محفوف بالمخاطر. قد يعني أن تغييراتنا قد تكسر شيئًا يعمل بالفعل. بسبب هذا القلق، نسعى جاهدين لجعل مدة هذه الحالة غير المستقرة قصيرة قدر الإمكان. هل تتذكر كيف قارنا TDD بركوب حصان يركض بسرعة؟ عندما يكون الحصان في الهواء (أي لا يلامس الأرض) فإنه يتقدم نحو هدفنا، لكنه ليس في حالة مستقرة. فقط عندما يلامس الحصان الأرض تستقر حالته. يشجع TDD على إجراء تغييرات صغيرة (أثناء الطيران) وتثبيت النظام على الفور عن طريق التحقق من عودته إلى الحالة المستقرة. نحن نقدر الحالة المستقرة على الرغم من أننا نتبنى التغييرات بحماس. بدون تغييرات، لن نتمكن من تقديم قيمة، ولكن يجب أن نفعل ذلك بطريقة متعمدة وحذرة للغاية. عند القيام بـ TDD، نتعامل مع التغييرات في الحالة المستقرة وكأننا نسير على قشر البيض. بغض النظر عن مدى تأكدنا من معرفة ما نفعله وكيف نفعله في هندسة البرمجيات، فمن الحكمة أن نترك الاختبارات الفاشلة توجه قراراتنا.

التحقق من نسبة الإكرامية الصحيحة المرتبطة بتقييم الخدمة

دعنا الآن ندخل تغييرًا آخر على تطبيقنا – اختبار للتحقق مما إذا كانت نسبة الإكرامية الصحيحة مرتبطة بتقييم الخدمة "Terrible". تذكر أننا قمنا بملء كائن Hashtable ratingPercentages بالقيم التالية:

ratingPercentages.Add( "Terrible" , 0 );

لقد كتبنا اختبارًا يتحقق من أن Hashtable ratingPercentages الخاص بنا يحتوي بالفعل على تقييم خدمة مشروع "Terrible". الآن نحتاج إلى اختبار يتحقق من أن تقييم الخدمة "Terrible" يعني أن نسبة الإكرامية لهذا التقييم هي 0.

[ Fact ]
public void CheckIfRatingTerribleHasZeroPercentTip ( )
{
    var expectedZeroPercentForTerribleRating = 0 ;
    var actualZeroPercentForTerribleRating = 10 ;
    Assert.Equal(expectedZeroPercentForTerribleRating, actualZeroPercentForTerribleRating);
}

يجب أن يفشل الاختبار الجديد CheckIfRatingTerribleHasZeroPercentTip:

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل اختبار نسبة الإكرامية الأولية

مرة أخرى، نقوم بتشفير قيم فعلية خاطئة عمدًا فقط حتى نتمكن من ملاحظة فشل اختبارنا الجديد تمامًا. الآن يجب علينا استبدال القيمة المشفرة بالاستدعاء الفعلي لدالة "حاسبة الإكرامية" (Tip Calculator) التي تعيد نسبة الإكرامية لتقييم الخدمة:

[ Fact ]
public void CheckIfRatingTerribleHasZeroPercentTip ( )
{
    var expectedZeroPercentForTerribleRating = 0 ;
    var actualZeroPercentForTerribleRating = tipCalculator.GetPercentageTipForRating( "Terrible" );
    Assert.Equal(expectedZeroPercentForTerribleRating, actualZeroPercentForTerribleRating);
}

كما في الحالة السابقة، لقد اخترعنا واجهة برمجة تطبيقات جديدة لـ "حاسبة الإكرامية" (Tip Calculator). نسمي هذه القدرة الجديدة GetPercentageTipForRating("Terrible"). إنها تأخذ قيمة تقييم الخدمة وتعيد نسبة الإكرامية لهذا التقييم. انتقل إلى ملف app/TipCalculator.cs وأضف الهيكل العظمي المشفر للدالة الجديدة:

 public int GetPercentageTipForRating ( string rating )
{
    return 10 ;
}

يفشل تشغيل الاختبار مرة أخرى، لأننا قمنا بتشفير قيمة الإرجاع. دعنا نستبدلها بالمعالجة الفعلية:

 public int GetPercentageTipForRating ( string rating )
{
    int tipPercentage = Int32.Parse(ratingPercentages[rating].ToString());
    return tipPercentage;
}

شغل الاختبار مرة أخرى:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح جميع الاختبارات الثلاثة

جميع الاختبارات الثلاثة ناجحة – نحن في المرحلة الخضراء، لقد عدنا إلى الحالة المستقرة!

ما هي نسبة الإكرامية التي نتوقعها لتقييمات الخدمة غير المشروعة؟

سنوات عديدة من الخبرة في هذا المجال علمتني أن أكون متشائمًا بعض الشيء. الآن بعد أن عاد تطبيقنا إلى الحالة المستقرة ويقدم قيمة (يجيب على أسئلة حول تقييمات الخدمة المشروعة ويعطينا أيضًا نسبة الإكرامية الصحيحة لتقييم "Terrible")، نحتاج إلى معرفة ما يحدث عندما نشغل تطبيقنا بإعطائه قيمة تقييم خدمة غير مشروعة (على سبيل المثال، بإعطائه تقييم خدمة "Whatever"). حان الوقت لمغادرة الحالة المستقرة مرة أخرى. سنكتب اختبارًا آخر:

[ Fact ]
public void CheckIfRatingWhateverHasNegativeOnePercentTip ( )
{
    var expectedZeroPercentForWhateverRating = -1 ;
    var actualZeroPercentForWhateverRating = tipCalculator.GetPercentageTipForRating( "Whatever" );
    Assert.Equal(expectedZeroPercentForWhateverRating, actualZeroPercentForWhateverRating);
}

نحن نصف توقعنا عندما يُطلب من "حاسبة الإكرامية" (Tip Calculator) إعادة نسبة الإكرامية لتقييم الخدمة "Whatever". نظرًا لأن تقييم الخدمة "Whatever" هو تقييم غير مشروع، فإننا نتوقع أن تعيد "حاسبة الإكرامية" (Tip Calculator) نسبة إكرامية بقيمة -1. هذا الاختبار الآن يحفز تحسينًا واحدًا في كود التطبيق الفعلي. نحتاج إلى إضافة بعض المنطق للتحقق أولاً مما إذا كان تقييم الخدمة المقدم مشروعًا أم لا. فقط إذا كان مشروعًا، نطلب من Hashtable ratingPercentages أن تخبرنا ما هي القيمة المرتبطة بنسبة الإكرامية. إذا كان تقييم الخدمة المقدم غير مشروع (على سبيل المثال، إذا كان "Whatever") فإننا نتجاوز التحدث إلى Hashtable ratingPercentages ونعيد ببساطة -1.

 public int GetPercentageTipForRating ( string rating )
{
    int tipPercentage = -1 ;
    if (CheckIfRatingIsValid(rating))
    {
        tipPercentage = Int32.Parse(ratingPercentages[rating].ToString());
    }
    return tipPercentage;
}

شغل الاختبارات، وجميع الاختبارات الأربعة ناجحة:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح جميع الاختبارات الأربعة

لقد عدنا إلى الحالة المستقرة. رحلة قصيرة أخرى إلى المنطقة المتقلبة، ونصر سريع آخر وعودة آمنة إلى حالة مستقرة لا تتأثر.

ملء نسب الإكرامية لتقييمات الخدمة الأخرى

الآن هو الوقت المناسب لأخذ قسط من الراحة وإجراء تغييرات أقل خطورة، باتباع النمط الذي تم تأسيسه بالفعل. اترك أمان الحالة المستقرة وقم برحلات قصيرة إلى المنطقة المتقلبة عن طريق إضافة اختبار جديد للتحقق مما إذا كان تقييم الخدمة "Poor" تقييمًا صالحًا ومشروعًا:

[ Fact ]
public void CheckIfRatingPoorIsValid ( )
{
    var expectedResponseForValidRating = true ;
    var actualResponseForValidRating = tipCalculator.CheckIfRatingIsValid( "Poor" );
    Assert.Equal(expectedResponseForValidRating, actualResponseForValidRating);
}

سيؤدي تشغيل هذا الاختبار إلى الفشل:

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل اختبار تقييم Poor

لم يتم تنفيذ تقييم الخدمة "Poor" بعد. لجعل الاختبار ينجح، قم بتنفيذ تقييم الخدمة "Poor" عن طريق إضافة هذا السطر إلى دالة البناء (constructor) لـ TipCalculator:

ratingPercentages.Add( "Poor" , 5 );

شغل الاختبارات، ونحن نعود إلى بر الأمان:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح جميع الاختبارات الستة

نحن نستمتع بحالة مستقرة مع 6 اختبارات ناجحة. الآن بعد أن أضفنا تقييم الخدمة "Poor" المرتبط بنسبة إكرامية 5%، دعنا نكتب اختبارًا يصف هذا التوقع:

[ Fact ]
public void CheckIfRatingPoorHasFivePercentTip ( )
{
    var expectedZeroPercentForPoorRating = 5 ;
    var actualZeroPercentForPoorRating = tipCalculator.GetPercentageTipForRating( "Poor" );
    Assert.Equal(expectedZeroPercentForPoorRating, actualZeroPercentForPoorRating);
}

تعمل الاختبارات بنجاح، ونحن نعود إلى بر الأمان في الحالة المستقرة. سأترك للقارئ إجراء التغييرات التي ستدفع تنفيذ تقييمات الخدمة "Good" و "Great" و "Excellent". في نهاية التمرين، يجب أن يعود نظامك إلى الحالة المستقرة مع 12 اختبارًا ناجحًا:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح 12 اختبارًا

حساب الإجمالي الكلي بناءً على الفاتورة وتقييم الخدمة

نحن الآن جاهزون للخطوة الأخيرة – بالنظر إلى الفاتورة الإجمالية وتقييم الخدمة، نتوقع من "حاسبة الإكرامية" (Tip Calculator) حساب نسبة الإكرامية وإضافتها إلى الإجمالي، لإنتاج الإجمالي الكلي الذي سيتم دفعه للمطعم. كما نفعل دائمًا، نصف التوقع أولاً:

[ Fact ]
public void CalculateTotalWithTip ( )
{
    var expectedTotalWithTip = 135.7 ;
    var actualTotalWithTip = 200.0 ;
    Assert.Equal(expectedTotalWithTip, actualTotalWithTip);
}

كالعادة، نقوم أولاً بتشفير بعض التوقعات التي نعرف أنها ستفشل. هذا لكي نلاحظ فشل اختبارنا الجديد:

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل اختبار CalculateTotalWithTip

حان الوقت لتنفيذ منطق المعالجة الذي سيحسب الإجمالي الصحيح مع الإكرامية. بالنظر إلى إجمالي 118.0 دولار، وتقييم الخدمة "Great" (إكرامية 15%)، نتوقع أن يكون الإجمالي 135.7 دولار:

[ Fact ]
public void CalculateTotalWithTip ( )
{
    var rating = "Great" ;
    var total = 118 ;
    var expectedTotalWithTip = 135.7 ;
    var actualTotalWithTip = tipCalculator.CalculateTotalWithTip(total, rating);
    Assert.Equal(expectedTotalWithTip, actualTotalWithTip);
}

لقد صممنا واجهة برمجة تطبيقات جديدة لـ "حاسبة الإكرامية" (Tip Calculator) – دالة تسمى CalculateTotalWithTip(total, rating). تأخذ قيمة الإجمالي وتقييم الخدمة وتعيد الإجمالي مع الإكرامية. يبدو تنفيذ الدالة كما يلي:

 public double CalculateTotalWithTip ( double total, string rating )
{
    double totalWithTip = -1 ;
    if (CheckIfRatingIsValid(rating))
    {
        int percentage = GetPercentageTipForRating(rating);
        totalWithTip = total + ((total/ 100 ) * percentage);
    }
    return totalWithTip;
}

شغل الاختبارات، ونحن نعود إلى الحالة المستقرة:

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح جميع الاختبارات بعد تنفيذ CalculateTotalWithTip

هل انتهينا هنا؟ لا، ليس بعد. حتى عندما تكون جميع الاختبارات في اللون الأخضر ونعود إلى الحالة المستقرة، لا يزال هناك شيئان نحتاج إلى القيام بهما. للبدء، نحتاج إلى إضافة توقع متشائم لحساب "حاسبة الإكرامية" (Tip Calculator) للإجمالي مع الإكرامية بناءً على تقييم الخدمة:

[ Fact ]
public void CalculateTotalWithTipForNonlegitimateRating ( )
{
    var rating = "Meh" ;
    var total = 118 ;
    var expectedTotalWithTip = 135.7 ;
    var actualTotalWithTip = tipCalculator.CalculateTotalWithTip(total, rating);
    Assert.Equal(expectedTotalWithTip, actualTotalWithTip);
}

يؤدي تشغيل الاختبارات إلى اختبار فاشل واحد:

لقطة شاشة لنتائج اختبار dotnet test تظهر فشل اختبار CalculateTotalWithTip لتقييم غير مشروع

كان توقعنا لتقييم الخدمة غير المشروع ("Meh") غير صحيح. الإجمالي الفعلي هو -1، لذا نحتاج إلى تعديل توقعنا عن طريق استبدال 135.7 بـ -1. شغل الاختبارات مرة أخرى، ونحن نعود إلى الحالة المستقرة!

لقطة شاشة لنتائج اختبار dotnet test تظهر نجاح جميع الاختبارات الـ 14

لدينا الآن 14 اختبارًا، جميعها ناجحة، و"حاسبة الإكرامية" (Tip Calculator) الخاصة بنا تعمل وفقًا لتوقعاتنا وتلبي معايير القبول. لقد انتهينا تقريبًا. تحقق أخير من السلامة قبل أن نتمكن بثقة من شحن "حاسبة الإكرامية" (Tip Calculator) الجديدة اللامعة – يجب علينا إجراء mutation testing. سيقوم إطار عمل اختبار التحول (mutation testing framework) الخاص بنا بتغيير كود التطبيق الفعلي، سطرًا واحدًا في كل مرة، وسيقوم بتشغيل جميع الاختبارات لكل تغيير فردي. إذا اشتكت الاختبارات من الكود المتحول، فكل شيء جيد، لقد قتلنا المتحول. إذا لم تشتك الاختبارات، فنحن في مشكلة. لدينا متحول باقٍ في قاعدة الكود الخاصة بنا، مما يعني أن هناك أسطرًا من الكود في مستودعنا تقوم بشيء لم نقدم له أي توقعات. دعنا نشغل mutation testing لنرى مدى صلابة حلنا. الأخبار الجيدة – حلنا قتل 100% من المتحولات!

لقطة شاشة لنتائج اختبار التحول تظهر قتل 100% من المتحولات

لقد أعطى mutation testing تطبيقنا الفعلي شهادة صحية نظيفة. يبدو أن "حاسبة الإكرامية" (Tip Calculator) الخاصة بنا في حالة جيدة.

دورة Red-Green-Refactor-Reflect في TDD

دعنا نراجع تمرين بناء "حاسبة الإكرامية" (Tip Calculator). بدأنا العملية بوصف توقعاتنا باستخدام تنسيق قصة المستخدم الكلاسيكي (user story format). تركز قصة المستخدم (كما يوحي الاسم) على وصف السيناريوهات التي تحقق أهداف المستخدم النهائي. في هذه الحالة، الهدف البسيط هو حساب مبلغ الإكرامية من تقييم الخدمة المقدم وإجمالي فاتورة المطعم. ثم يُضاف مبلغ الإكرامية المحسوب تلقائيًا إلى الإجمالي.

من هناك، شرعنا في بناء تطبيقنا الفعلي باتباع منهجية TDD. كما أوضحنا، تتكون المنهجية من كتابة اختبار فاشل، وملاحظة فشله (مرحلة Red في TDD)، ثم إجراء تغييرات فورية على الكود لضمان نجاح الاختبار (مرحلة Green في TDD). بمجرد نجاح الاختبار، ننتقل إلى مرحلة إعادة الهيكلة (Refactor) (نعيد هيكلة الكود دون التأثير على سلوكه). بهذه الطريقة، نتأكد من أن كودنا ليس مكلفًا للتغيير. تتطلب ممارسة TDD الصحيحة أيضًا مراجعة متكررة – نسميها انعكاسًا (reflection). نتوقف ونفكر في الأشياء التي أنجزناها حتى الآن، لمعرفة ما إذا كان بإمكاننا التعلم من تجاربنا الأخيرة. هذا الانعكاس يعزز العملية، لأنه يعتمد على التغذية الراجعة المتكررة والمحكمة التي توفرها الاختبارات الفاشلة، ثم الناجحة.

لقد قارنت بالفعل تطوير البرمجيات الموجه بالاختبار (Test Driven Development) بتجربة ركوب حصان يركض بسرعة. أثناء ركوب الحصان، نتبادل بين الطيران في الهواء (أي السرعة التي تتحقق عندما يقفز الحصان من الأرض) وتوجيه الحصان. من المستحيل توجيه الحصان بينما نحن بعيدون عن الأرض، في الهواء. في تلك المرحلة، نكتسب سرعة، لكن لا يمكننا إجراء أي تغييرات في الاتجاه. فقط عندما يلامس الحصان الأرض يمكننا إجراء تغيير في الاتجاه. في TDD، نسعى جاهدين لملامسة الأرض قدر الإمكان. كلما كانت القفزات أطول دون ملامسة الأرض، قلّت فرصتنا لتصحيح المسار.

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

لماذا التركيز على "لا تكتب جميع الاختبارات أولاً"؟

يدعي العديد من مهندسي البرمجيات غير المطلعين على ممارسات Agile كما تُطبق في TDD، إما أن كتابة الاختبارات الآلية ليست ضرورية، أو أن الاختبارات الآلية يجب أن تُكتب بعد اكتمال الكود. بمجرد أن يبدأوا في التعرف على Agile و TDD، قد يعيدون النظر في ممارساتهم ويقررون أن كتابة الاختبارات قبل كتابة كود التنفيذ قد يكون أكثر منطقية. ومع ذلك، بسبب عقلية الشلال (waterfall mentality) المتأصلة، يرتكب بعض هؤلاء المهندسين خطأ كتابة جميع الاختبارات أولاً، ثم ينتقلون فقط إلى كتابة الكود. هذا النهج خاطئ تمامًا. إنه يعادل نهج الشلال التقليدي حيث نمر بعملية التطوير باحترام المراحل المقيدة ببوابات.

أولاً نكتب المتطلبات (في هذه الحالة، ستكون المتطلبات توقعات مكتوبة في شكل اختبارات آلية). فقط بمجرد كتابة جميع المتطلبات (أي الاختبارات الآلية)، والتوقيع عليها، وتجميدها، ننتقل إلى المرحلة المقيدة التالية – كتابة الكود لتطبيق الشحن. TDD هو عكس تمامًا لنهج "كتابة الاختبارات أولاً". في TDD، نكتب دائمًا اختبارًا واحدًا فقط. يصف هذا الاختبار سلوكًا مرغوبًا. السلوك المرغوب فيه غير موجود بعد (ولهذا السبب هو مرغوب فيه)، ويفشل الاختبار. ثم ننتقل على الفور إلى إجراء تغييرات على الكود في محاولة لإنشاء السلوك المرغوب فيه. بمجرد إنشاء السلوك المرغوب فيه، يتم التحقق منه بواسطة الاختبار، وإذا تم تلبية توقعات الاختبار، ننتقل إلى إعادة هيكلة الكود (refactoring) (لتلبية المتطلبات غير الوظيفية (nonfunctional requirements)، مثل تكلفة التغيير).

نحن نمارس انضباطًا صارمًا لعدم الاستسلام أبدًا لإغراء كتابة أكثر من اختبار واحد في كل مرة. بهذه الطريقة، نضمن أننا نواصل ملامسة الأرض قدر الإمكان. نفضل البقاء "في الطيران" لأقصر فترة ممكنة. نحن "في الطيران" خلال تلك الفترة التي لم يتحقق فيها السلوك المتوقع والمرغوب فيه الموصوف في الاختبار بعد. كلما كان السلوك المتوقع والمرغوب فيه أصغر، كلما كان مسارنا "في الطيران" أقصر. بهذه الطريقة، نواصل ملامسة الأرض كثيرًا، مما يمنحنا فرصة لتعديل التوجيه.

الخلاصة

يُعد بناء "حاسبة إكرامية" (Tip Calculator) بسيطة مشكلة صغيرة، واستخدام هذا التمرين لتوضيح منهجية TDD لا يقدم بالضرورة حجة مقنعة لصالح TDD. ومع ذلك، ضمن قيود مقال تقني، قد يوفر هذا التمرين العملي رؤى قيمة حول فوائد اعتماد TDD. ما زلنا نرى أن الفوائد الحقيقية لـ TDD لا تظهر إلا عند التعامل مع جهود هندسة برمجيات أكبر وأكثر تعقيدًا بكثير. إن القدرة على البقاء راسخًا أثناء إجراء تغييرات قد تكون محفوفة بالمخاطر على نظام كبير ومعقد غالبًا ما تكون منقذة للحياة. بالإضافة إلى ذلك، يؤدي بناء البرمجيات باستخدام منهجية TDD إلى تقليل كبير في إعادة العمل. يدفع TDD درجة عالية من التجزئة (modularization)، مما يؤدي إلى تماسك عالٍ للوحدات (modules) وربط منخفض بينها. تنتج كل هذه الخصائص تطبيقًا فعليًا يكون كود مصدره سهلًا وغير مكلف للتغيير. وقد ثبت أن خفض تكلفة التغيير هو أفضل طريقة نحو تبني التغييرات والتخلي عن المفهوم المعروف باسم "زحف النطاق" (scope creep). باختصار، يمكّن TDD فرق هندسة البرمجيات من تقديم درجة عالية من المرونة للأعمال.

الخلاصة التقنية

يُبرز هذا المقال ببراعة الفلسفة الأساسية لـ TDD التي تتجاوز مجرد "كتابة الاختبارات أولاً" لتصل إلى "كتابة اختبار واحد في كل مرة". إن التركيز على دورة Red-Green-Refactor-Reflect يوضح كيف أن TDD ليس مجرد أداة لضمان الجودة، بل هو منهجية تصميم قوية تدفع نحو كود أكثر نظافة، ووحداتية (modularity)، وقابلية للصيانة. إن مقارنة TDD بركوب الحصان الذي يلامس الأرض بشكل متكرر، مقابل الطائرة الورقية التي تظل في الهواء، تقدم استعارة ممتازة لتوضيح أهمية التغذية الراجعة الفورية والتحقق المستمر. هذه المنهجية لا تقلل فقط من مخاطر التغيير في الأنظمة المعقدة، بل تعزز أيضًا فهم المطورين للمتطلبات وتصميم الحلول، مما يؤدي إلى منتج برمجي أكثر قوة ومرونة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *