PHPUnit. "كيف يمكنني اختبار وحدة تحكم لعن بلدي" ، أو اختبار للالمشككين

مرحبا يا هبر.

صورة

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

وهكذا ، فإن googling السريع حول موضوع الاختبارات يجد الكثير من المقالات التي تنقسم في مجموعتها إلى فئتين:

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

<?php class Calculator { public function plus($a, $b) { return $a + $b; } } 

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

2) أمثلة متطورة للغاية. ودعنا نكتب اختبارًا ، ونحشره في Gitlab CI ، ثم سنقوم بإصلاحه تلقائيًا إذا نجح الاختبار ، وسنطبق PHP Infection على الاختبارات ، لكننا سنربط كل شيء بهدسون. وهكذا في هذا النمط. يبدو ذلك مفيدًا ، لكن يبدو أنه لا يبحث عن أي شيء على الإطلاق. لكنك تريد فقط زيادة الاستقرار في مشروعك بشكل طفيف. وكل هذه الاستمرارات - حسناً ، ليس كلها مرة واحدة.

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

بادئ ذي بدء ، برنامج تعليمي قصير في شكل سؤال وجواب:

س: هل يجب علي استخدام نوع من الإطار؟ ماذا لو كان لدي يي؟ ماذا لو كوهانا؟ ماذا لو كان٪ one_more_framework_name٪؟
ج: لا ، PHPUnit هو إطار اختبار مستقل ، حتى يمكنك تثبيته على الكود القديم في إطار عمل عصامي.

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

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

صورة

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

 function isValid($email) { $regex = "very_complex_regex_here"; if (is_array($email)) { $result = true; foreach ($email as $item) { if (preg_match($regex, $item) === 0) { $result = false; } } } else { $result = preg_match($regex, $emai) ==! 0; } return $result; } 

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

س: متى سأبدأ برؤية بعض المعنى من الاختبارات؟
ج: أولاً ، بمجرد تغطية جزء كبير من الشفرة. كلما كانت التغطية أقرب إلى 100٪ ، كان الاختبار أكثر موثوقية. ثانياً ، بمجرد قيامك بإجراء تغييرات عمومية ، أو تغييرات في الجزء المعقد من الكود. يمكن أن تختبر الاختبارات المشكلات التي يمكن تفويتها يدويًا بسهولة (حالات الحدود). ثالثًا ، عند كتابة الاختبارات بأنفسهم! غالبًا ما يكون هناك موقف عند كتابة اختبار يكشف عن عيوب في الكود غير مرئية للوهلة الأولى.

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

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

 <?php class Controller { public function __construct($userService, $emailService) { $this->userService = $userService; $this->emailService = $emailService; } public function login($request) { if (empty($request->login) || empty($request->password)) { return "Auth error"; } $password = $this->userService->getPasswordFor($request->login); if (empty($password)) { return "Auth error - no password"; } if ($password !== $request->password) { return "Incorrect password"; } $this->emailService->sendEmail($request->login); return "Success"; } } // .... /* somewhere in project core */ $controller = new Controller($userService, $emailService); $controller->login($request); 

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

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

 $userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service'); 

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

 function testEmptyLogin() { $userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service'); $controller = new Controller($userService, $emailService); $request = (object) []; $result = $controller->login($request); } 

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

 function testEmptyLogin() { $userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service'); $controller = new Controller($userService, $emailService); $request = (object) []; $result = $controller->login($request); // vv assertion here! vv $this->assertEquals("Auth error", $result); } 

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

 function testEmptyPassword() { $userService = Mockery::mock('user_service'); // $userService->getPasswordFor(__any__arg__); // '' $userService->shouldReceive('getPasswordFor')->andReturn(''); $emailService = Mockery::mock('email_service'); $request = (object) [ 'login' => 'john', 'pass' => '1234' ]; $result = (new Controller($userService, $emailService))->login($request); $this->assertEquals("Auth error - no password", $result); } function testUncorrectPassword() { $userService = Mockery::mock('user_service'); // $userService->getPasswordFor(__any__arg__); // '4321' $userService->shouldReceive('getPasswordFor')->andReturn('4321'); $emailService = Mockery::mock('email_service'); $request = (object) [ 'login' => 'john', 'pass' => '1234' ]; $result = (new Controller($userService, $emailService))->login($request); $this->assertEquals("Incorrect password", $result); } function testSuccessfullLogin() { $userService = Mockery::mock('user_service'); // $userService->getPasswordFor(__any__arg__); // '1234' $userService->shouldReceive('getPasswordFor')->andReturn('1234'); $emailService = Mockery::mock('email_service'); $request = (object) [ 'login' => 'john', 'pass' => '1234' ]; $result = (new Controller($userService, $emailService))->login($request); $this->assertEquals("Success", $result); } 

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

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

صورة - كل الاختبارات كانت ناجحة. أو شيء من هذا القبيل

صورة فشل أحد التأكيدات السبعة.

"هذا ، بالطبع ، رائع ، لكن ماذا عن هذا لا يمكنني أن أضع يدي فيه؟" ودعونا نتخيل الكود التالي لهذا

 <?php function getInfo($infoApi, $userName) { $response = $infoApi->getInfo($userName); if ($response->status === "API Error") { return null; } return $response->result; } // ... somewhere in system $api = new ExternalApi(); $info = getInfo($api, 'John'); if ($info === null) { die('Api is down'); } echo $info; 

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

 function getInfo($infoApi, $userName): string { $response = $infoApi->getInfo($userName); if ($response->status === "API Error") { throw new ApiException($response); } return $response->result; } 

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

 function testApiFail() { $api = Mockery::mock('api'); $api->shouldReceive('getInfo')->andReturn((object) [ 'status' => 'API Error' ]); $result = getInfo($api, 'name'); $this->assertNull($result); } 

هذه المعلومات هي في الواقع كافية. إذا لم يكن لديك شعرية قديمة ، فيمكنك كتابة أول اختبار بعد 20-30 دقيقة. وبعد بضعة أسابيع - لتعلم شيئًا جديدًا رائعًا ، ارجع إلى التعليقات الموجودة أسفل هذا المنشور ، واكتب المؤلف الذي لا يعرف govnokoder عنه٪ framework_name٪ ، ويكتب اختبارات سيئة ، لكن عليك القيام به٪ this_way٪. وسوف أكون سعيدًا جدًا في هذه الحالة. هذا يعني أن هدفي قد تحقق: اكتشف شخص آخر الاختبار لنفسه ، وزاد المستوى الاحترافي العام في مجالنا قليلاً!

النقد المنطقي مرحب به.

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


All Articles