التطوير المدفوع بالاختبار: ما هو وما هو ليس كذلك.

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

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

إذا شعرت بهذه الطريقة ، أعتقد أنك قد لا تفهم ما هو TDD حقًا. (حسنًا ، الجملة السابقة كانت لجذب انتباهك). يوجد كتاب جيد جدًا عن TDD ، "التطوير المستند إلى الاختبار": على سبيل المثال ، من تأليف Kent Beck ، إذا كنت تريد التحقق منه ومعرفة المزيد.

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

لماذا تستخدم TDD؟

هناك دراسات وأوراق ومناقشات حول مدى فعالية TDD. على الرغم من أنه من المفيد بالتأكيد الحصول على بعض الأرقام ، لا أعتقد أنها تجيب على سؤال لماذا يجب علينا استخدام TDD في المقام الأول.

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

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

تأتي الإجابة الأطول مما هو عليه حقًا TDD ... لنبدأ بالقواعد.

قواعد اللعبة

يصف العم بوب TDD بثلاث قواعد:

- لا يُسمح لك بكتابة أي رمز إنتاج ما لم يكن عليك اجتياز اختبار الوحدة الفاشل. - لا يُسمح لك بكتابة أي اختبار وحدة أكثر مما يكفي للفشل ؛ وفشل التجميع هو فشل. - لا يُسمح لك بكتابة أي كود إنتاج أكثر مما هو كاف لاجتياز اختبار الوحدة الفاشلة.

أحب أيضًا إصدارًا أقصر وجدته هنا:

- اكتب فقط ما يكفي من اختبار الوحدة للفشل. - اكتب فقط ما يكفي من رمز الإنتاج لاجتياز اختبار الوحدة الفاشلة.

هذه القواعد بسيطة ، لكن الأشخاص الذين يقتربون من TDD غالبًا ما ينتهكون واحدًا أو أكثر منها. أتحداك: هل يمكنك كتابة مشروع صغير باتباع هذه القواعد بدقة ؟ أعني بالمشروع الصغير شيئًا حقيقيًا ، وليس مجرد مثال يتطلب مثل 50 سطرًا من التعليمات البرمجية.

تحدد هذه القواعد آليات TDD ، لكنها بالتأكيد ليست كل ما تحتاج إلى معرفته. في الواقع ، غالبًا ما توصف عملية استخدام TDD بأنها دورة أحمر / أخضر / إعادة فاكتور. دعونا نرى ما يدور حوله.

دورة إعادة فاكتور الأحمر والأخضر

المرحلة الحمراء

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

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

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

تمثل كل مرحلة من مراحل دورة RGR مرحلة في دورة حياة الكود وكيف يمكنك الارتباط بها.

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

في هذه المرحلة ، تركز على كتابة واجهة نظيفة للمستخدمين في المستقبل. هذه هي المرحلة التي تصمم فيها كيف سيستخدم العملاء الكود الخاص بك.

هذه القاعدة الأولى هي الأكثر أهمية وهي القاعدة التي تجعل TDD مختلفًا عن الاختبار العادي. تكتب اختبارًا بحيث يمكنك بعد ذلك كتابة رمز الإنتاج. أنت لا تكتب اختبارًا لاختبار الكود الخاص بك.

لنلقي نظرة على مثال.

// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});

الكود أعلاه هو مثال لكيفية ظهور الاختبار في JavaScript ، باستخدام إطار اختبار Jasmine. لا تحتاج إلى معرفة الياسمين - يكفي أن تفهم أن it(...)هذا اختبار expect(...).toBe(...)وطريقة لجعل الياسمين يتحقق مما إذا كان هناك شيء ما كما هو متوقع.

في الاختبار أعلاه ، تحققت من أن الدالة LeapYear.isLeap(...)تعود trueلعام 1996. قد تعتقد أن عام 1996 هو رقم سحري وبالتالي فهو ممارسة سيئة. ليس. في كود الاختبار ، تكون الأرقام السحرية جيدة ، بينما يجب تجنبها في كود الإنتاج.

هذا الاختبار له في الواقع بعض الآثار:

  • اسم آلة حاسبة السنة الكبيسة هو LeapYear
  • isLeap(...)هي طريقة ثابتة لـ LeapYear
  • isLeap(...)يأخذ رقمًا (وليس مصفوفة ، على سبيل المثال) كوسيطة ويعيد trueأو false.

إنه اختبار واحد ، لكنه في الواقع له العديد من الآثار! هل نحتاج إلى طريقة لمعرفة ما إذا كانت السنة سنة كبيسة ، أم أننا بحاجة إلى طريقة تُرجع قائمة بالسنوات الكبيسة بين تاريخ البدء وتاريخ الانتهاء؟ هل اسم العناصر له معنى؟ هذه هي أنواع الأسئلة التي يجب أن تضعها في اعتبارك أثناء كتابة الاختبارات في المرحلة الحمراء.

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

هنا يأتي خطأ آخر: لا تكتب مجموعة من الوظائف / الفئات التي تعتقد أنك قد تحتاجها. ركز على الميزة التي تقوم بتنفيذها وعلى ما هو مطلوب حقًا. كتابة شيء لا تتطلبه الميزة هو مبالغة في الهندسة.

ماذا عن التجريد؟ سنرى ذلك لاحقًا ، في مرحلة إعادة البناء.

المرحلة الخضراء

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

هنا يأتي خطأ كبير آخر: بدلاً من كتابة كود كافٍ لاجتياز الاختبار الأحمر ، تكتب كل الخوارزميات. أثناء القيام بذلك ، ربما تفكر في ما هو التنفيذ الأكثر أداءً. لا يمكن!

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

لكن لماذا لدينا هذه القاعدة؟ لماذا لا يمكنني كتابة كل الكود الموجود بالفعل في ذهني؟ لسببين:

  • المهمة البسيطة أقل عرضة للأخطاء ، وتريد تقليل الأخطاء.
  • أنت بالتأكيد لا تريد الخلط بين التعليمات البرمجية التي هي قيد الاختبار مع التعليمات البرمجية التي ليست كذلك. يمكنك كتابة كود ليس قيد الاختبار (المعروف أيضًا باسم قديم) ، ولكن أسوأ شيء يمكنك القيام به هو الخلط بين التعليمات البرمجية المختبرة وغير المختبرة.

ماذا عن الكود النظيف؟ ماذا عن الأداء؟ ماذا لو جعلتني كتابة الكود أكتشف مشكلة؟ ماذا عن الشكوك؟

الأداء قصة طويلة ، وهو خارج نطاق هذه المقالة. دعنا نقول فقط أن ضبط الأداء في هذه المرحلة هو ، في معظم الأحيان ، تحسين سابق لأوانه.

توفر تقنية التطوير المدفوعة بالاختبار شيئين آخرين: قائمة المهام ومرحلة إعادة البناء.

The refactor phase is used to clean up the code. The to-do list is used to write down the steps required to complete the feature you are implementing. It also contains doubts or problems you discover during the process. A possible to-do list for the leap year calculator could be:

Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4- but not by 100- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?

The to-do list is live: it changes while you are coding and, ideally, at the end of the feature implementation it will be blank.

Refactor phase

In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication. Kent Becks suggests in his book that removing code duplication is all you need to do.

In this phase you play the part of a picky programmer who wants to fix/refactor the code to bring it to a professional level. In the red phase, you’re showing off your skills to your users. But in the refactor phase, you’re showing off your skills to the programmers who will read your implementation.

Removing code duplication often results in abstraction. A typical example is when you move two pieces of similar code into a helper class that works for both the functions/classes where the code has been removed.

For example the following code:

class Hello { greet() { return new Promise((resolve) => { setTimeout(()=>resolve('Hello'), 100); }); }}class Random { toss() { return new Promise((resolve) => { setTimeout(()=>resolve(Math.random()), 200); }); }}new Hello().greet().then(result => console.log(result));new Random().toss().then(result => console.log(result));

could be refactored into:

class Hello { greet() { return PromiseHelper.timeout(100).then(() => 'hello'); }}class Random { toss() { return PromiseHelper.timeout(200).then(() => Math.random()); }}class PromiseHelper { static timeout(delay) { return new Promise(resolve => setTimeout(resolve, delay)); }}const logResult = result => console.log(result);new Hello().greet().then(logResult);new Random().toss().then(logResult);

As you can see, in order to remove thenew Promise and setTimeout code duplication, I created a PromiseHelper.timeout(delay) method, which serves both Hello and Random classes.

Just keep in mind that you cannot move to another test unless you’ve removed all the code duplication.

Final considerations

In this section I will try to answer to some common questions and misconceptions about Test Drive Development.

  • T.D.D. requires much more time than “normal” programming!

What actually requires a lot of time is learning/mastering TDD as well as understanding how to set up and use a testing environment. When you are familiar with the testing tools and the TDD technique, it actually doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.

  • How many test do I have to write?

The minimum amount that lets you write all the production code. The minimum amount, because every test slows down refactoring (when you change production code, you have to fix all the failing tests). On the other hand, refactoring is much simpler and safer on code under tests.

  • With Test Driven Development I don’t need to spend time on analysis and on designing the architecture.

This cannot be more false. If what you are going to implement is not well-designed, at a certain point you will think “Ouch! I didn’t consider…”. And this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.

  • Should test coverage be 100%?

No. As I said earlier, don’t mix up tested and untested code. But you can avoid using TDD on some parts of a project. For example I don’t test views (although a lot of frameworks make UI testing easy) because they are likely to change often. I also ensure that there is very a little logic inside views.

  • I am able to write code with very a few bugs, I don’t need testing.

You may able to to that, but is the same consideration valid for all your team members? They will eventually modify your code and break it. It would be nice if you wrote tests so that a bug can be spotted immediately and not in production.

  • TDD works well on examples, but in a real application a lot of the code is not testable.

I wrote a whole Tetris (as well as progressive web apps at work) using TDD. If you test first, code is clearly testable. It is more a matter of understanding how to mock dependencies and how to write simple but effective tests.

  • Tests should not be written by the developers who write the code, they should be written by others, possibly QA people.

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

ماذا بعد؟

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