اختبار الوحدة و Python



اسمي Vadim ، وأنا مطور رائد في Mail.Ru Search. سوف أشارك خبرتنا في اختبار الوحدة. تتكون المقالة من ثلاثة أجزاء: في الأول سأخبرك بما نحققه بشكل عام بمساعدة اختبار الوحدة ؛ يصف الجزء الثاني المبادئ التي نتبعها ؛ ومن الجزء الثالث ستتعلم كيف يتم تطبيق المبادئ المذكورة في Python.

الأهداف


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

في مشاريعنا ، نسعى وراء العديد من الأهداف.

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

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

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

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

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

المبادئ


إذا فهمت أهدافك ، يمكنك فهم ما يجب القيام به لتحقيقها. وهنا تبدأ المشاكل. والحقيقة هي أنه تم كتابة الكثير من الكتب والمقالات في اختبار الوحدة ، ولكن النظرية لا تزال غير ناضجة للغاية.

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

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

تحدث إلى أي مُختبِر: سيخبرك أنه من خلال الاختبار اليدوي يتخيل دائمًا التنفيذ. من تجربته ، يفهم تمامًا أين يخطئ المبرمجون عادةً. لا يقوم المختبر بفحص كل شيء ، أولاً يدخل 5 ، ثم 6 ، ثم 7. يتحقق من 5 ، abc ، –7 ، والرقم لـ 100 حرف ، لأنه يعرف أن تنفيذ هذه القيم قد يختلف ، ولكن لـ 6 و 7 من غير المحتمل .

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

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

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

تنفيذ بايثون


نحن نستخدم مكتبة unittest القياسية من عائلة xUnit. القصة هي: كانت هناك لغة SmallTalk ، وفيها مكتبة SUnit. الجميع أحبها ، بدأوا في نسخها. تم استيراد المكتبة إلى Java تحت اسم Junit ، من هناك في C ++ تحت اسم CppUnit وإلى Ruby تحت اسم RUnit (ثم أعيدت تسميتها إلى RSpec). أخيرًا ، من جافا ، انتقلت المكتبة إلى Python تحت اسم unittest. وقاموا باستيراده حرفيا لدرجة أنه حتى CamelCase بقي ، على الرغم من أن هذا لا يتوافق مع PEP 8.

حول xUnit هناك كتاب رائع ، "أنماط اختبار xUnit". يصف كيفية العمل مع أطر هذه الأسرة. العيب الوحيد للكتاب هو حجمه: إنه ضخم ، ولكن حوالي 2/3 من المحتويات عبارة عن كتالوج للأنماط. والثلث الأول من الكتاب رائع ، هذا أحد أفضل الكتب التي قابلتُها في مجال تكنولوجيا المعلومات.

اختبار الوحدة هو رمز عادي له بنية قياسية معينة. تتكون جميع اختبارات الوحدة من ثلاث مراحل: الإعداد والتمرين والتحقق. تقوم بإعداد البيانات وتشغيل الاختبارات ومعرفة ما إذا كان كل شيء قد وصل إلى الحالة الصحيحة.



الإعداد


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

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

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

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

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

أما بالنسبة لنظام الملفات ، فإن التزييف أمر بسيط للغاية. هناك وحدة io مع io.StringIO و io.BytesIO . يمكنك إنشاء كائنات تشبه الملف ولا تصل فعليًا إلى القرص. ولكن إذا كان هذا الأمر مفاجئًا بالنسبة لك ، فهناك وحدة tempfile رائعة مع مديري السياق للملفات المؤقتة والأدلة والملفات المسماة وأي شيء. Tempfile هو وحدة فائقة إذا لم يكن IO مناسبًا لك لسبب ما.

مع قاعدة البيانات ، كل شيء أكثر تعقيدًا. هناك توصية قياسية: "لا تستخدم قاعدة حقيقية ، بل قاعدة مزيفة." أنا لا أعرف عنك ، ولكن في حياتي لم أر قاعدة واحدة مزيفة وعملية بما فيه الكفاية. في كل مرة كنت أطلب المشورة بشأن ما يجب اتخاذه على وجه التحديد تحت Python أو Perl ، أجابوا أنه لا أحد يعرف أي شيء جاهز ، وعرضوا كتابة شيء خاص بهم. لا أستطيع أن أتخيل كيف يمكنك كتابة محاكي ، على سبيل المثال ، PostgreSQL. نصيحة أخرى: "ثم احصل على SQLite". لكن هذا سيكسر العزلة ، لأن SQLite يعمل مع نظام الملفات. بالإضافة إلى ذلك ، إذا كنت تستخدم شيئًا مثل MySQL أو PostgreSQL ، فمن المحتمل ألا يعمل SQLite. إذا بدا لك أنك لا تستخدم الإمكانيات المحددة لمنتجات معينة ، فأنت على الأرجح مخطئ. بالتأكيد حتى بالنسبة للأشياء الشائعة ، مثل العمل مع التواريخ ، فإنك تستخدم ميزات محددة يدعمها نظام إدارة قواعد البيانات (DBMS) فقط.

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

الوضع الأسوأ قليلاً هو عندما يُطلب منك تشغيل قاعدة بيانات محلية ، والتي سيتم استخدامها. لكن السؤال هو ، كيف ستصل البيانات إلى هناك؟ لقد قلنا بالفعل أنه يجب أن يكون هناك بعض الحالة الأولية للنظام ، يجب أن يكون هناك بعض البيانات في قاعدة البيانات. من أين جاءوا ليس سؤالا سهلا.

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

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

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

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

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

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

تمرن


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

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

تحقق


في هذه المرحلة ، نستخدم بنشاط تأكيدات مكتوبة ذاتيا ، حتى أحادية الخط. إذا قمت باختبار وجود ملف في الاختبار ، فبدلاً من تأكيد self.assertTrue(file_exists(f)) أوصي بكتابة تأكيد not file exists . يرتبط Holivar بهذا: هل يجب أن أستمر في استخدام CamelCase في الأسماء ، كما هو الحال في unittest ، أم يجب أن أتبع PEP 8؟ ليس لدي إجابة. إذا اتبعت PEP 8 ، فسيكون في رمز الاختبار فوضى من CamelCase و snake_case. وإذا كنت تستخدم CamelCase ، فهذا لا يتوافق مع PEP 8.

والأخير. افترض أن لديك رمزًا يختبر شيئًا ، وهناك العديد من خيارات البيانات التي يجب تشغيل هذا الرمز عليها. إذا كنت تستخدم py.test ، يمكنك إجراء نفس الاختبار باستخدام بيانات إدخال مختلفة. إذا لم يكن لديك py.test ، فيمكنك استخدام مثل هذا الديكور . يتم تمرير طاولة إلى الديكور ، ويتحول اختبار واحد إلى عدة اختبارات أخرى ، كل منها يختبر إحدى الحالات.

الخلاصة


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

لا تتردد في استخدام اختبارات التبعية. لا حرج في ذلك. إذا رفعت memcached ، لأنه بدونها لا يعمل رمزك بشكل طبيعي ، فلا بأس. ولكن من الأفضل الاستغناء عنها ، إن أمكن.

انتبه للمصانع. هذا نمط مثير للغاية.

PS أدعوك إلى قناة Telegram لمؤلفي للبرمجة في Python -pythonetc.

Source: https://habr.com/ru/post/ar418929/


All Articles