عند كتابة اختبارات الوحدة ، لا تستخدم السخرية
نشرت: 2018-05-02ملاحظة: هذا هو أحدث منشور هندسي تقني كتبه المهندس الرئيسي Seth Ammons. شكر خاص لسام نجوين ، كين كيم ، إلمر توماس ، وكيفن جيليت لمراجعة الزملاء لهذا المنشور . و أو أكثر من منشورات مثل هذه ، تحقق من قائمة المدونة الفنية الخاصة بنا.
أنا أستمتع حقًا بكتابة الاختبارات الخاصة بشفري ، خاصة اختبارات الوحدة. الشعور بالثقة الذي يمنحني إياه رائع. إن الحصول على شيء لم أعمل عليه منذ وقت طويل والقدرة على تشغيل اختبارات الوحدة والتكامل يمنحني المعرفة بأنه يمكنني إعادة البناء بلا رحمة إذا لزم الأمر ، وطالما أن اختباراتي تتمتع بتغطية جيدة وذات مغزى وتستمر في النجاح ، لا يزال لدي برنامج وظيفي بعد ذلك.
توجه اختبارات الوحدة تصميم الكود وتسمح لنا بالتحقق بسرعة من أن أوضاع الفشل والتدفقات المنطقية تعمل على النحو المنشود. مع ذلك ، أريد أن أكتب عن شيء ربما يكون أكثر إثارة للجدل بعض الشيء: عند كتابة اختبارات الوحدة ، لا تستخدم السخريات.
دعنا نحصل على بعض التعاريف على الطاولة
ما الفرق بين اختبارات الوحدة واختبار التكامل؟ ماذا أعني بالسخافات وما الذي يجب أن تستخدمه بدلاً من ذلك؟ يركز هذا المنشور على العمل في Go ، وبالتالي فإن ميلي إلى هذه الكلمات يقع في سياق Go.
عندما أقول اختبارات الوحدة ، فإنني أشير إلى تلك الاختبارات التي تضمن معالجة الأخطاء بشكل مناسب وتوجيه تصميم النظام عن طريق اختبار وحدات صغيرة من التعليمات البرمجية. بالوحدة ، يمكن أن نشير إلى حزمة كاملة أو واجهة أو طريقة فردية.
اختبار التكامل هو المكان الذي تتفاعل فيه فعليًا مع الأنظمة و / أو المكتبات التابعة. عندما أقول "mocks" ، فأنا أشير على وجه التحديد إلى مصطلح "Mock Object" ، حيث "نستبدل رمز النطاق بالتطبيقات الوهمية التي تحاكي الوظائف الحقيقية وتفرض التأكيدات حول سلوك الكود الخاص بنا [1]" (التركيز الخاص بي).
تم ذكره بشكل أقصر قليلاً: يقوم السخرية بتأكيد السلوك ، مثل:
MyMock.Method ("foo"). يسمى (1) .WithArgs ("bar"). إرجاع ("raz")
أنا أدافع عن "المنتجات المقلدة بدلاً من السخرية".
المزيف هو نوع من الاختبار المزدوج الذي قد يحتوي على سلوك تجاري [2]. المزيفة هي مجرد هياكل تناسب الواجهة وهي شكل من أشكال حقن التبعية حيث نتحكم في السلوك. الفائدة الرئيسية من المنتجات المقلدة هي أنها تقلل من اقتران الكود ، حيث تزيد المحاكاة من الاقتران ، ويؤدي الاقتران إلى صعوبة إعادة البناء [3].
في هذا المنشور ، أعتزم إثبات أن المنتجات المقلدة توفر المرونة وتسمح بسهولة الاختبار وإعادة البناء. إنها تقلل التبعيات مقارنة بالسخرية ، ويسهل صيانتها.
دعنا نتعمق في مثال أكثر تقدمًا قليلاً من "اختبار دالة المجموع" كما قد ترى في منشور نموذجي من هذا النوع. ومع ذلك ، أحتاج إلى إعطائك بعض السياق حتى تتمكن من فهم الكود التالي في هذا المنشور بسهولة أكبر.
في SendGrid ، كان لدى أحد أنظمتنا ملفات على نظام الملفات المحلي بشكل تقليدي ، ولكن نظرًا للحاجة إلى توفر أعلى وإنتاجية أفضل ، فإننا نقوم بنقل هذه الملفات إلى S3.
لدينا تطبيق يحتاج إلى أن يكون قادرًا على قراءة هذه الملفات واخترنا تطبيقًا يمكن تشغيله في وضعين "محلي" أو "بعيد" ، اعتمادًا على التكوين. التحذير الذي تم استبعاده في العديد من نماذج التعليمات البرمجية هو أنه في حالة حدوث فشل عن بُعد ، نعود إلى قراءة الملف محليًا.
مع هذا بعيدًا ، يحتوي هذا التطبيق على أداة تجميع حزم. نحتاج إلى التأكد من أن برنامج getter للحزمة يمكنه الحصول على الملفات إما من نظام الملفات البعيد أو نظام الملفات المحلي.
نهج ساذج: فقط اتصل بالمكتبة والمكالمات على مستوى النظام
تتمثل الطريقة الساذجة في أن حزمة التنفيذ الخاصة بنا ستستدعي getter.New (...) وتمريرها المعلومات اللازمة لإعداد إما الحصول على ملف محلي أو بعيد وستقوم بإرجاع Getter . ستتمكن القيمة التي تم إرجاعها بعد ذلك من استدعاء MyGetter.GetFile (...) مع المعلمات اللازمة لتحديد موقع الملف البعيد أو المحلي.
هذا سوف يعطينا هيكلنا الأساسي. عندما نقوم بإنشاء Getter الجديد ، نقوم بتهيئة المعلمات المطلوبة لأي ملف بعيد محتمل (مفتاح وصول وسر) ونمرر أيضًا بعض القيم التي تنشأ في تكوين التطبيق الخاص بنا ، مثل useRemoteFS التي ستخبر الكود بمحاولة نظام الملفات البعيد.
نحن بحاجة إلى توفير بعض الوظائف الأساسية. تحقق من الكود الساذج هنا [4] ؛ أدناه نسخة مخفضة. لاحظ أن هذا مثال غير مكتمل وسنقوم بإعادة هيكلة الأشياء.
الفكرة الأساسية هنا هي أنه إذا تم تكويننا للقراءة من نظام الملفات البعيد وحصلنا على تفاصيل نظام الملفات عن بُعد (المضيف ، والحاوية ، والمفتاح) ، فيجب أن نحاول القراءة من نظام الملفات البعيد. بعد أن نثق في قراءة النظام عن بُعد ، سنقوم بتحويل كل قراءة الملفات إلى نظام الملفات البعيد وإزالة الإشارات إلى القراءة من نظام الملفات المحلي.
هذا الرمز ليس سهل اختبار الوحدة ؛ لاحظ أنه للتحقق من كيفية عمله ، نحتاج في الواقع إلى الوصول ليس فقط إلى نظام الملفات المحلي ، ولكن أيضًا على نظام الملفات البعيد. الآن ، يمكننا فقط إجراء اختبار تكامل وإعداد بعض Docker magic للحصول على مثيل s3 يسمح لنا بالتحقق من المسار السعيد في الكود.
يعد إجراء اختبار التكامل فقط أقل من مثالي على الرغم من أن اختبارات الوحدة تساعدنا في تصميم برامج أكثر قوة عن طريق اختبار التعليمات البرمجية البديلة ومسارات الفشل بسهولة. يجب علينا حفظ اختبارات التكامل لأنواع أكبر من الاختبارات "هل تعمل حقًا". في الوقت الحالي ، دعنا نركز على اختبارات الوحدة.
كيف يمكننا جعل هذا الرمز أكثر قابلية للاختبار؟ هناك مدرستان فكريتان. الأول هو استخدام مولد وهمي (مثل https://github.com/vektra/mockery أو https://github.com/golang/mock) الذي ينشئ رمزًا معياريًا للاستخدام عند اختبار النماذج.
يمكنك السير في هذا المسار وإنشاء استدعاءات نظام الملفات واستدعاءات عميل Minio. أو ربما ترغب في تجنب التبعية ، لذلك تقوم بإنشاء السخائر الخاصة بك يدويًا. اتضح أن الاستهزاء بعميل Minio ليس أمرًا سهلاً لأن لديك عميلاً مكتوبًا بشكل ملموس يقوم بإرجاع كائن مكتوب بشكل ملموس.
أقول إن هناك طريقة أفضل من السخرية. إذا أعدنا هيكلة الكود الخاص بنا ليكون أكثر قابلية للاختبار ، فلن نحتاج إلى عمليات استيراد إضافية للسجلات والتطبيقات ذات الصلة ولن تكون هناك حاجة لمعرفة المزيد من اختبارات DSL لاختبار الواجهات بثقة. يمكننا إعداد الكود الخاص بنا بحيث لا يتم اقترانه بشكل مفرط وسيكون كود الاختبار مجرد كود Go عادي باستخدام واجهات Go. دعنا نقوم به!
نهج الواجهة: تجريد أكبر واختبار أسهل
ما الذي نحتاج إلى اختباره؟ هذا هو المكان الذي يخطئ فيه بعض Gophers الجدد. لقد رأيت الناس يفهمون قيمة الاستفادة من الواجهات ، لكنهم يشعرون أنهم بحاجة إلى واجهات تتوافق مع التنفيذ الملموس للحزمة التي يستخدمونها.
قد يرون أن لدينا عميل Minio ، لذلك قد يبدأون بإنشاء واجهات تطابق جميع أساليب واستخدامات عميل Minio (أو أي عميل s3 آخر). إنهم ينسون قول Go Proverb [5] [6] "كلما كبرت الواجهة ، كان التجريد أضعف."
لا نحتاج إلى اختبار ضد عميل Minio. نحتاج إلى اختبار أنه يمكننا الحصول على الملفات عن بُعد أو محليًا (والتحقق من بعض مسارات الفشل ، مثل حالات الفشل عن بُعد). دعنا نعيد صياغة هذا النهج الأولي وسحب عميل Minio إلى جهاز تجميع عن بُعد. أثناء قيامنا بذلك ، لنفعل الشيء نفسه مع الكود الخاص بنا لقراءة الملف المحلي ، ونصنع أداة جمع محلية. فيما يلي الواجهات الأساسية ، وسيكون لدينا نوع لتنفيذ كل منها:

مع وجود هذه الأفكار التجريدية في مكانها الصحيح ، يمكننا إعادة صياغة تنفيذنا الأولي. سنقوم بوضع localFetcher و remoteFetcher على بنية Getter وإعادة بناء GetFile لاستخدامهما. تحقق من النسخة الكاملة من الكود المعاد بنائه هنا [7]. يوجد أدناه مقتطف مبسط قليلاً باستخدام إصدار الواجهة الجديد:
هذا الرمز الجديد المعاد بناؤه أكثر قابلية للاختبار للوحدات لأننا نأخذ الواجهات كمعلمات في بنية Getter ويمكننا تغيير الأنواع الملموسة إلى أنواع مزيفة. بدلاً من الاستهزاء بمكالمات نظام التشغيل أو الحاجة إلى الاستهزاء الكامل بعميل Minio أو واجهات كبيرة ، نحتاج فقط إلى مزيفين بسيطين: fakeLocalFetcher و fakeRemoteFetcher .
تحتوي هذه المنتجات المقلدة على بعض الخصائص التي تتيح لنا تحديد ما يتم إرجاعه. سنكون قادرين على إرجاع بيانات الملف أو أي خطأ نحبّه ويمكننا التحقق من أن طريقة GetFile الاستدعاء تتعامل مع البيانات والأخطاء كما أردنا.
مع وضع هذا في الاعتبار ، يصبح قلب الاختبارات:
مع هذا الهيكل الأساسي ، يمكننا أن نختتم كل شيء في اختبارات مدفوعة الجدول [8]. سيتم اختبار كل حالة في جدول الاختبارات للوصول إلى الملفات المحلية أو عن بُعد. سنكون قادرين على إدخال خطأ في الوصول إلى الملفات عن بعد أو المحلي. يمكننا التحقق من الأخطاء التي تم نشرها ، وأن محتويات الملف قد تم تجاوزها ، وأن إدخالات السجل المتوقعة موجودة.
لقد تقدمت وقمت بتضمين جميع حالات الاختبار والتباديل المحتملة في الاختبار القائم على الجدول الواحد المتاح هنا [9] (يمكنك ملاحظة أن بعض تواقيع الطرق مختلفة قليلاً - فهي تتيح لنا القيام بأشياء مثل إدخال أداة تسجيل والتأكيد مقابل بيانات السجل ).
أنيق ، إيه؟ لدينا سيطرة كاملة على الطريقة التي نريد أن يتصرف بها GetFile ، ويمكننا التأكيد على النتائج. لقد صممنا الكود الخاص بنا ليكون ملائمًا لاختبار الوحدات ويمكننا الآن التحقق من مسارات النجاح والأخطاء المطبقة في طريقة GetFile .
يقترن الكود بشكل فضفاض ويجب أن تكون إعادة البناء في المستقبل نسيمًا. لقد فعلنا ذلك من خلال كتابة كود ol 'Go البسيط بحيث يتمكن أي مطور على دراية بـ Go من فهمه وتوسيعه عند الحاجة.
Mocks: ماذا عن تفاصيل التنفيذ الدقيقة والشجاعة؟
ما الذي ستشتريه لنا السخريات ولا نحصل عليه في الحل المقترح؟ يمكن أن يكون السؤال الرائع الذي يعرض فائدة لعمل محاكاة تقليدية ، "كيف تعرف أنك اتصلت بعميل s3 باستخدام المعلمات الصحيحة؟ باستخدام mocks ، يمكنني التأكد من أنني قمت بتمرير قيمة المفتاح إلى معلمة المفتاح ، وليس معلمة الحاوية. "
هذا مصدر قلق صالح ويجب تغطيته تحت اختبار في مكان ما . نهج الاختبار الذي أدافع عنه هنا لا يتحقق من أنك اتصلت بعميل Minio باستخدام الحاوية والمعلمات الرئيسية بالترتيب الصحيح.
قال اقتباس رائع قرأته مؤخرًا ، "الاستهزاء يقدم افتراضات ، والتي تقدم مخاطر [10]". أنت تفترض أن مكتبة العميل تم تنفيذها بشكل صحيح ، وتفترض أن جميع الحدود صلبة ، وتفترض أنك تعرف كيف تتصرف المكتبة بالفعل.
الاستهزاء بالمكتبة يسخر فقط من الافتراضات ويجعل اختباراتك أكثر هشاشة وعرضة للتغيير عند تحديث الشفرة (وهو ما خلص إليه مارتن فاولر في Mocks Arn't Stubs [3]). عندما يلتقي المطاط بالطريق ، سيتعين علينا التحقق من أننا بالفعل نستخدم عميل Minio بشكل صحيح وهذا يعني اختبارات التكامل (قد تعيش هذه في إعداد Docker أو بيئة اختبار). نظرًا لأنه سيكون لدينا كل من اختبارات الوحدة والتكامل ، فليس هناك حاجة لاختبار وحدة لتغطية التنفيذ الدقيق لأن اختبار التكامل سيغطي ذلك.
في مثالنا ، توجه اختبارات الوحدة تصميم الكود الخاص بنا وتسمح لنا باختبار أن الأخطاء والتدفقات المنطقية تعمل على النحو المصمم ، وتفعل بالضبط ما يحتاجون إلى القيام به.
بالنسبة للبعض ، يشعرون أن هذه ليست تغطية اختبار وحدة كافية. إنهم قلقون بشأن النقاط أعلاه. سيصر البعض على واجهات نمط الدمية الروسية حيث تقوم إحدى الواجهات بإرجاع واجهة أخرى تعيد واجهة أخرى ، ربما مثل ما يلي:
وبعد ذلك قد يسحبون كل جزء من عميل Minio في كل غلاف ثم يستخدمون مولدًا وهميًا (إضافة التبعيات إلى البنيات والاختبارات ، وزيادة الافتراضات ، وجعل الأشياء أكثر هشاشة). في النهاية ، سيتمكن الساخر من قول شيء مثل:
myClientMock.ExpectsCall ("GetObject"). إرجاع (mockObject) .NumberOfCalls (1) .WithArgs (مفتاح ، دلو) - وهذا إذا كان بإمكانك استدعاء التعويذة الصحيحة لهذا DSL المحدد.
سيكون هذا كثيرًا من التجريد الإضافي المرتبط مباشرة باختيار التنفيذ لاستخدام عميل Minio. سيؤدي هذا إلى اختبارات هشة عندما نكتشف أننا بحاجة إلى تغيير افتراضاتنا حول العميل ، أو نحتاج إلى عميل مختلف تمامًا.
يضيف هذا إلى وقت تطوير الكود الشامل الآن وفي المستقبل ، ويزيد من تعقيد الكود ويقلل من قابلية القراءة ، ويحتمل أن يزيد التبعيات على المولدات الوهمية ، ويعطينا القيمة الإضافية المشكوك فيها لمعرفة ما إذا قمنا بخلط الحاوية والمعلمات الرئيسية التي كنا سنكتشفها في اختبار التكامل على أي حال.
مع إدخال المزيد والمزيد من العناصر ، تصبح أداة التوصيل أكثر إحكامًا وإحكامًا. ربما نكون قد صنعنا مسجلاً وهميًا وبعد ذلك بدأنا في الحصول على مقاييس وهمية. قبل أن تعرف ذلك ، فأنت تقوم بإضافة إدخال سجل أو مقياس جديد وقمت للتو بكسر عدد لا يحصى من الاختبارات التي لم تتوقع ظهور مقياس إضافي.
في المرة الأخيرة التي تعرضت فيها لهذا الأمر في Go ، لن يخبرني إطار العمل المحاكي حتى ما هو الاختبار أو الملف الذي فشل لأنه أصيب بالذعر ومات موتًا فظيعًا لأنه جاء عبر مقياس جديد (هذا يتطلب بحثًا ثنائيًا في الاختبارات من خلال التعليق عليها حتى نتمكن من العثور على المكان الذي نحتاج إليه لتغيير السلوك الوهمي). هل يمكن للسخرية أن تضيف قيمة؟ بالتأكيد. هل يستحق هذه التكلفة؟ في معظم الحالات ، لست مقتنعًا.
واجهات: البساطة واختبار الوحدة للفوز
لقد أظهرنا أنه يمكننا توجيه التصميم والتأكد من اتباع التعليمات البرمجية الصحيحة ومسارات الخطأ باستخدام بسيط للواجهات في Go. من خلال كتابة ملفات مزيفة بسيطة تلتزم بالواجهات ، يمكننا أن نرى أننا لا نحتاج إلى نماذج وهمية أو أطر عمل أو مولدات وهمية لإنشاء كود مصمم للاختبار. لقد لاحظنا أيضًا أن اختبار الوحدة ليس كل شيء ، ويجب عليك كتابة اختبارات تكامل للتأكد من أن الأنظمة تتكامل بشكل صحيح مع بعضها البعض.
آمل أن أحصل على منشور حول بعض الطرق الرائعة لإجراء اختبارات التكامل في المستقبل ؛ ابقوا متابعين!
مراجع
1: الاختبار الداخلي: اختبار الوحدة باستخدام كائنات وهمية (2000): انظر المقدمة لمعرفة تعريف الكائن المثير للسخرية
2: The Little Mocker: انظر إلى الجزء المتعلق بالمنتجات المقلدة ، على وجه التحديد ، "للمزيف سلوك تجاري. يمكنك دفع شخص مزيف إلى التصرف بطرق مختلفة من خلال إعطائه بيانات مختلفة ".
3: المهاجرون ليسوا Stubs: انظر القسم ، "إذن هل يجب أن أكون كلاسيكيًا أم ساخرًا؟" صرح مارتن فاولر ، "لا أرى أي فوائد مقنعة لـ TDD الساخر ، وأنا قلق بشأن عواقب اختبارات الاقتران على التنفيذ."
4: نهج ساذج: نسخة مبسطة من الكود. انظر [7].
5: https://go-proverbs.github.io/: قائمة Go Proverbs مع روابط للمحادثات.
6: https://www.youtube.com/watch؟v=PAAkCSZUG1c&t=5m17s: رابط مباشر للتحدث بواسطة Rob Pike فيما يتعلق بحجم الواجهة والتجريد.
7: النسخة الكاملة من الكود التجريبي: يمكنك استنساخ الريبو وتشغيل "go test".
8: الاختبارات المعتمدة على الجدول: استراتيجية اختبار لتنظيم كود الاختبار لتقليل الازدواجية.
9: اختبارات للنسخة الكاملة من الكود التجريبي. يمكنك تشغيلها باستخدام "اختبار الانتقال".
10: أسئلة تطرحها على نفسك عند كتابة الاختبارات بقلم ميشال شاريمزا: الاستهزاء يقدم افتراضات ، والافتراضات تعرض المخاطر.
