في عام 2003 ، أصدر Derick Rethans Xdebug 1.2 . لأول مرة في نظام PHP البيئي ، من الممكن جمع بيانات تغطية الكود. في عام 2004 ، أطلق سيباستيان بيرجمان PHPUnit 2 ، حيث استخدمها لأول مرة. يتمتع المطورون الآن بالقدرة على قياس أداء مجموعات الاختبار الخاصة بهم باستخدام تقارير التغطية.
منذ ذلك الحين ، تم نقل الوظيفة إلى مكون تغطية كود php عام ومستقل . ظهرت PHPDBG و PCOV كبرامج تشغيل بديلة . لكن بشكل أساسي ، لم تتغير العملية الأساسية للمطورين على مدار الـ 16 عامًا الماضية.
في أغسطس 2020 ، مع إصدار تغطية php-code-9.0 والإصدارات ذات الصلة PHPUnit 9.3 و behat-code-cover 5.0 ، أصبحت طريقة جديدة لتقدير التغطية متاحة.
اليوم سننظر
- جولة سريعة في الأساسيات
- محددات
- المقاييس البديلة
- تغطية الفرع
- تغطية المسارات
- بما في ذلك المقاييس الجديدة
- أي مقياس يجب استخدامه؟
- هل هناك أي أسباب لعدم تضمين المقاييس الجديدة؟
- النتيجة
جولة سريعة في الأساسيات
معظم مطوري PHP على دراية بفكرة اختبار الكود الآلي. ترتبط فكرة تغطية الكود ارتباطًا وثيقًا بالاختبار الآلي وتتعلق بقياس النسبة المئوية للشفرة التي تم تنفيذها أو ، كما يقولون ، "مغطاة" بالاختبارات. على سبيل المثال ، إذا كان لديك الكود التالي:
<?php
class PercentCalculator
{
public function __construct(int $numerator, int $denominator)
{
$this->numerator = $numerator;
$this->denominator = $denominator;
}
public function calculatePercent(): float
{
return round($this->numerator / $this->denominator * 100, 1);
}
}
ثم يمكننا كتابة اختبار PHPUnit كما هو موضح أدناه:
<?php
class PercentCalculatorTest extends PHPUnit\Framework\TestCase
{
public function testTwentyIntoForty(): void
{
$calculator = new PercentCalculator(20, 40);
self::assertEquals(50.0, $calculator->calculatePercent());
}
}
بعد إجراء الاختبار ، تؤكد PHPUnit أننا وصلنا إلى تغطية 100٪ في هذا المثال البسيط:
محددات
ومع ذلك ، في المثال أعلاه ، كان هناك خطأ بسيط محتمل. إذا $ القاسم هو 0 ، ثم نحصل على القسمة على صفر خطأ. دعنا نصلح ذلك ونرى ما يحدث:
<?php
class PercentCalculator
{
public function __construct(int $numerator, int $denominator)
{
$this->numerator = $numerator;
$this->denominator = $denominator;
}
public function calculatePercent(): float
{
// ,
//
//
return $this->denominator ? round($this->numerator / $this->denominator * 100, 1) : 0.0;
}
}
على الرغم من أن السطر 12 يستخدم الآن العبارة الثلاثية if / else (ولم نكتب حتى اختبارًا للتحقق من صحة معالجتنا الفارغة) ، يخبرنا التقرير أنه لا يزال لدينا تغطية رمز بنسبة 100٪.
إذا تمت تغطية جزء من الخط بواسطة الاختبار ، فسيتم تمييز الخط بالكامل على أنه مغطى . هذا يمكن أن يكون مضللا!
من خلال حساب ما إذا كان السطر قد تم تنفيذه أم لا ، يمكن أن تواجه بنيات الكود الأخرى نفس المشكلات ، على سبيل المثال:
if ($a || $b || $c) { // **
doSomething(); // 100%
}
public function pluralise(string $thing, int $count): string
{
$string = $count . ' ' . $thing;
if ($count > 1) { // $count >= 2, - 100%
$string .= 's'; // $count === 1,
} // ,
return $string;
}
المقاييس البديلة
بدءًا من الإصدار 2.3 ، كان Xdebug قادرًا على جمع ليس فقط المقاييس المألوفة سطرًا بسطر ، ولكن أيضًا مقاييس تغطية الفروع والمسار البديلة. انتهت مشاركة مدونة ديريك التي تحدثت عن هذه الميزة ببيان سيئ السمعة:
"يبقى الانتظار حتى يتاح لسيباستيان (أو أي شخص آخر) الوقت لتحديث PHP_CodeCoverage لإظهار تغطية الفرع والمسار. قرصنة سعيدة!
ديريك ريتانس ، يناير 2015 "
بعد 5 سنوات من انتظار هذا "الشخص الآخر" الغامض ، قررت أن أحاول تطبيقه بنفسي. شكراً جزيلاً لسيباستيان بيرغمان لقبول طلب السحب .
تغطية الفرع
في جميع الكودات باستثناء أبسطها ، هناك أماكن يمكن أن يتباعد فيها مسار التنفيذ إلى مسارين أو أكثر. يحدث هذا في كل نقطة قرار ، مثل كل حالة أو وقت . كل جانب من نقاط الاختلاف هذه هو فرع منفصل. إذا لم يكن هناك نقطة قرار ، فإن سلسلة التنفيذ تحتوي على فرع واحد فقط.
لاحظ أنه على الرغم من استخدام استعارة الشجرة ، فإن الفرع في هذا السياق يختلف عن فرع التحكم في الإصدار ، فلا تخلط بين الاثنين!
عندما يتم تمكين تغطية الفرع والمسار ، يتم إنشاء تقرير HTML بتغطية كود php، بالإضافة إلى تقرير تغطية الخط العادي ، يتضمن الوظائف الإضافية لعرض تغطية الفرع والمسار. هذا هو الشكل الذي تبدو عليه تغطية الفرع باستخدام نفس مثال الكود كما في السابق:
كما ترى ، يشير المربع المحوري في أعلى الصفحة على الفور إلى أنه على الرغم من أن لدينا تغطية كاملة سطرًا بسطر ، فإن هذا لا ينطبق على تغطية الفرع والمسار ( يتم مناقشة المسارات بالتفصيل في القسم التالي).
بالإضافة إلى ذلك ، يتم تمييز السطر 12 باللون الأصفر للإشارة إلى أنه يحتوي على تغطية غير كاملة (سيتم عرض خط بتغطية 0٪ باللون الأحمر كالمعتاد).
أخيرًا ، كلما زاد الانتباه ، قد يلاحظ أنه على عكس تغطية سطر بسطر ، يتم تمييز المزيد من الخطوط بالألوان. هذا لأن الفروع تُحسب بناءً على تدفق التنفيذ داخل مترجم PHP. يبدأ الفرع الأول لكل دالة عند إدخال هذه الوظيفة. هذا على عكس التغطية المستندة إلى السلسلة ، حيث يتم اعتبار جسم الوظيفة فقط يحتوي على سلاسل قابلة للتنفيذ ، ويعتبر إعلان الوظيفة نفسه غير قابل للتنفيذ.
البحث عن الفروع
مثل هذه الاختلافات بين ما يعتبره مترجم PHP فرعًا منفصلاً منطقيًا من الكود والنموذج العقلي للمطور يمكن أن يجعل من الصعب فهم المقاييس. على سبيل المثال ، إذا سألتني عن عدد الفروع في حساب النسبة المئوية () ، فسأجيب عن 2 (حالة خاصة لـ 0 والحالة العامة). ومع ذلك ، بالنظر إلى تقرير تغطية كود php أعلاه ، فإن هذه الوظيفة المكونة من سطر واحد تحتوي بالفعل على ... 4 فروع؟!
لفهم ما يعنيه مترجم PHP ، يوجد تقرير تغطية إضافي تحت المنبع. يعرض نسخة موسعة من عرض كل فرع ، مما يساعد على تحديد المخفي في الكود المصدري بكفاءة أكبر. تبدو هكذا:
تقول التسمية التوضيحية: "فيما يلي سطور المصدر التي تمثل كل فرع من الكود الذي وجده Xdebug . لاحظ أنه لا يجب أن يكون الفرع هو نفسه السلسلة: يمكن أن تحتوي السلسلة النصية على عدة فروع وبالتالي تظهر أكثر من مرة. ضع في اعتبارك أيضًا أن بعض الفروع يمكن أن تكون ضمنية ، على سبيل المثال ، تحتوي عبارة if دائمًا على عنصر آخر في التدفق المنطقي ، حتى لو لم تكتبها ".
كل هذا ليس واضحًا تمامًا حتى الآن ، ولكن يمكنك بالفعل فهم الفروع الموجودة بالفعل في حساب النسبة المئوية () :
- يبدأ الفرع 1 عند إدخال الوظيفة ويتضمن $ this-> فحص المقام ؛
- ثم يتم تقسيم التنفيذ إلى فرعين 2 و 3 حسب ما إذا تم التعامل مع الحالة الخاصة أم لا ؛
- الفرع 4 هو المكان الذي يتم فيه دمج الفرعين 2 و 3. وهو يتكون من العودة والخروج من الوظيفة.
تعتبر مطابقة الفروع ذهنيًا مع الأجزاء الفردية من الكود المصدري مهارة جديدة تتطلب القليل من الممارسة. لكن القيام بذلك باستخدام رمز سهل القراءة والفهم هو بالتأكيد أسهل. إذا كانت شفرتك مليئة بخطوط ذكية واحدة تجمع بين عدة أجزاء من المنطق ، كما في مثالنا ، فتوقع المزيد من التعقيد مقارنةً بالكود حيث يتم تنظيم كل شيء وكتابته في عدة أسطر ، بما يتوافق تمامًا مع الفروع. سيبدو نفس المنطق المكتوب بهذا الأسلوب كما يلي:
زهرة البرسيم
في حالة تصدير فب رمز التغطية التقرير في البرسيم شكل لنقلها إلى نظام آخر، ثم مع تمكين تغطية القائم على فرع، ستتم كتابة البيانات إلى الشرطية و مفاتيح coveredconditionals . في السابق (أو إذا لم يتم تمكين تغطية الفرع) ، كانت القيم المصدرة صفرًا دائمًا.
تغطية المسارات
المسارات هي مجموعات محتملة من الفروع. مثال calculatePercent () له مساران محتملان ، كما هو موضح أعلاه:
- الفرع 1 ، ثم الفرع 2 ، ثم الفرع 4 ؛
- الفرع 1 ، ثم الفرع 3 ، ثم الفرع 4.
ومع ذلك ، غالبًا ما يكون عدد المسارات أكبر من عدد الفروع ، على سبيل المثال ، في التعليمات البرمجية التي تحتوي على العديد من الشروط والحلقات. المثال التالي ، المأخوذ من تغطية كود php ، له 23 فرعًا ، لكن يوجد بالفعل 65 مسارًا مختلفًا للدالة:
final class File extends AbstractNode
{
public function numberOfTestedMethods(): int
{
if ($this->numTestedMethods === null) {
$this->numTestedMethods = 0;
foreach ($this->classes as $class) {
foreach ($class['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
foreach ($this->traits as $trait) {
foreach ($trait['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
}
return $this->numTestedMethods;
}
}
إذا لم تتمكن من العثور على جميع الفروع الثلاثة والعشرين ، فتذكر أن foreach يمكنه قبول مكرر فارغ ، وإذا كان هناك دائمًا عنصر آخر غير مرئي .
نعم ، هذا يعني أن هناك حاجة إلى 65 اختبارًا لتغطية 100٪.
يتضمن تقرير HTML الخاص بتغطية كود php ، مثل الفروع ، عرضًا إضافيًا لكل مسار. يظهر أي منها مغطاة بالعجين وأي منها غير مغطاة.
حماقة
يؤثر تمكين تغطية المسار بشكل أكبر على المقاييس المعروضة ، وهي نتيجة CRAP . يستخدم التعريف المنشور على crap4j.org مقياس تغطية المسار غير المتاح تاريخيًا في PHP كمدخلات للحساب . بينما في PHP ، تم دائمًا استخدام تغطية سطر بسطر. بالنسبة إلى الميزات الصغيرة ذات التغطية الجيدة ، من المرجح أن تظل درجة CRAP كما هي أو حتى تنخفض. ولكن بالنسبة للوظائف التي تحتوي على العديد من مسارات التنفيذ والتغطية السيئة ، ستزيد القيمة بشكل كبير.
بما في ذلك المقاييس الجديدة
يتم تمكين أو تعطيل تغطية الفرع والمسار معًا ، نظرًا لأن كلاهما يمثل ببساطة تمثيلات مختلفة لنفس بيانات تنفيذ التعليمات البرمجية الأساسية.
وحدة PHP
بالنسبة إلى PHPUnit 9.3+ ، يتم تعطيل المقاييس الإضافية افتراضيًا ويمكن تمكينها إما من خلال سطر الأوامر أو من خلال ملف تكوين phpunit.xml ، ولكن فقط عند التشغيل ضمن Xdebug . ستؤدي محاولة تمكين هذه الميزة عند استخدام PCOV أو PHPDBG إلى تحذير من عدم توافق التكوين ولن يتم جمع التغطية.
- في وحدة التحكم ، استخدم خيار تغطية المسار - : vendor / bin / phpunit - تغطية المسار .
- في phpunit.xml ، اضبط سمة pathCoverage لعنصر التغطية على true .
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage pathCoverage="true" processUncoveredFiles="true" cacheDirectory="build/phpunit/cache">
<include>
<directory suffix=".php">src</directory>
</include>
<report>
<text outputFile="php://stdout"/>
<html outputDirectory="build/coverage"/>
</report>
</coverage>
</phpunit>
في PHPUnit 9.3 ، تم تغيير تنسيق ملف التكوين بشكل خطير ، لذا من المحتمل أن تبدو البنية أعلاه مختلفة عما اعتدت عليه.
تغطية كود behat
بالنسبة لـ behat-code-cover 5.0+ ، يتم الإعداد في behat.yml ، وتسمى السمة BranchAndPathCoverage . إذا حاولت تمكينه باستخدام برنامج تشغيل آخر غير Xdebug ، فسيتم إصدار تحذير ، ولكن سيستمر إنشاء التغطية. هذا لتسهيل استخدام نفس ملف التكوين في بيئات مختلفة. إذا لم يتم تكوينها بشكل صريح ، سيتم تمكين التغطية الجديدة بشكل افتراضي عند التشغيل تحت Xdebug .
أي مقياس يجب استخدامه؟
أنا شخصيًا ( دوغ رايت ) سأستخدم المقاييس الجديدة كلما أمكن ذلك. لقد اختبرتهم على كود مختلف لمعرفة ما هو "المعيار". في مشاريعي ، على الأرجح ، سأستخدم نهجًا هجينًا ، سأعرضه أدناه. بالنسبة للمشاريع التجارية ، من الواضح أن قرار التحول إلى المقاييس الجديدة يجب أن يتخذ من قبل الفريق بأكمله ، وأنا أتطلع إلى فرصة لمقارنة نتائجهم مع نتائجي.
رأيي
التغطية القائمة على المسار بنسبة 100٪ هي بلا شك الكأس المقدسة ، وحيث يكون من المنطقي تطبيقها فهي مقياس جيد يجب السعي لتحقيقه حتى لو لم تفعل ذلك. إذا كتبت اختبارات ، فلا يزال عليك التفكير في أشياء مثل حالات الحافة. تساعدك التغطية القائمة على المسار على التأكد من أنه بخير.
ومع ذلك ، إذا احتوت الطريقة على عشرات أو مئات أو حتى آلاف المسارات (وهو أمر شائع في الواقع للأشياء المعقدة إلى حد ما) ، فلن أضيع الوقت في كتابة مئات الاختبارات. من الحكمة أن تتوقف عند العاشرة. الاختبار ليس غاية في حد ذاته ، ولكنه أداة لتخفيف المخاطر واستثمار في المستقبل. يجب أن تؤتي الاختبارات ثمارها ، والوقت الذي يقضيه في ذلكالاختبارات من غير المرجح أن تؤتي ثمارها. في مثل هذه المواقف ، من الأفضل أن تهدف إلى تغطية فرع جيدة ، لأنها على الأقل تضمن لك التفكير فيما يحدث في كل نقطة قرار.
في حالات عدد كبير من المسارات (تم تعريفها الآن جيدًا باستخدام CRAP الصادق) ، أقوم بتقييم ما إذا كانت الشفرة المعنية لا تفعل الكثير ، وهل هناك طريقة معقولة لتقسيمها إلى وظائف أصغر (والتي يمكن تحليلها بالفعل بمزيد من التفصيل)؟ في بعض الأحيان لا ، ولا بأس بذلك - لسنا بحاجة للتخلص من جميع مخاطر المشروع تمامًا. حتى معرفتهم أمر رائع. من المهم أيضًا أن تتذكر أن حدود الوظائف واختبار الوحدة المعزولة الخاصة بها هي فصل اصطناعي للمنطق ، وليس التعقيد الحقيقي لبرنامجك العام. لذلك ، أوصي بعدم كسر الوظائف الكبيرة لمجرد العدد الهائل من مسارات التنفيذ. افعل ذلك فقط عندما يقلل الفصل من الحمل المعرفي ويساعد على إدراك الكود.
هل هناك أي أسباب لعدم تضمين المقاييس الجديدة؟
نعم ، الأداء. ليس سراً أن كود Xdebug بطيء بشكل لا يصدق مقارنة بأداء PHP العادي . وإذا قمت بتشغيل تغطية الفروع والمسارات ، فإن كل شيء يتفاقم بإضافة المصاريف العامة لجميع بيانات التنفيذ الإضافية التي يحتاج الآن إلى تتبعها.
والخبر السار هو أن الاضطرار إلى معالجة هذه المشكلات قد ألهم المطور لإجراء تحسينات عامة في الأداء ضمن تغطية كود php والتي ستفيد أي شخص يستخدم Xdebug . يختلف أداء مجموعات الاختبار اختلافًا كبيرًا ، لذلك من الصعب الحكم على كيفية تأثير ذلك على كل مجموعة اختبار ، ولكن جمع التغطية المستندة إلى السلسلة سيكون أسرع على أي حال.
لا يزال إنشاء تغطية من الفروع والمسارات أبطأ بحوالي 3-5 مرات. يجب أن يؤخذ هذا في الاعتبار. ضع في اعتبارك التمكين الانتقائي لملفات الاختبار الفردية بدلاً من مجموعة الاختبار بأكملها ، أو إنشاء "تغطية أفضل" ليلاً بدلاً من تشغيل كل دفعة. سيكون
Xdebug 3 أسرع بكثير من الإصدارات الحالية بسبب العمل المنجز على الوحدات النمطية والأداء ، لذلك يجب اعتبار هذه التحذيرات على أنها خاصة بـ Xdebug 2 فقط . مع الإصدار 3 ، حتى مع مراعاة النفقات العامة لجمع البيانات الإضافية ، من الممكن إنشاء تغطية قائمة على الفروع وقائمة على المسار في وقت أقل مما يستغرقه اليوم للحصول على تغطية سطر بسطر!
الاختبارات التي أجراها سيباستيان بيرجمان ، رسم بياني رسمه ديريك ريثانس
النتيجة
يرجى اختبار الميزات الجديدة والكتابة إلينا. هل هم مفيدون؟ تعتبر أفكار التصور البديل (ربما من لغات أخرى) مثيرة للاهتمام بشكل خاص.
حسنًا ، أنا مهتم دائمًا برأيك حول المستوى الطبيعي لتغطية الكود.
في PHP Russia في 29 نوفمبر ، سنناقش جميع الأسئلة الأكثر أهمية حول تطوير PHP ، حول ما هو غير موجود في التوثيق ، ولكن ما الذي سيعطي الكود الخاص بك مستوى جديدًا.
انضم إلينا في المؤتمر: ليس فقط للاستماع إلى التقارير وطرح الأسئلة على أفضل المتحدثين في عالم PHP ، ولكن أيضًا للتواصل المهني (أخيرًا غير متصل!) في جو دافئ. مجتمعاتنا: Telegram و Facebook و VKontakte و YouTube .