يجب أن تلهم اختبارات الكتابة الثقة في التشغيل الصحيح للكود. غالبًا ما نعمل على درجة تغطية الرمز ، وعندما نصل إلى 100٪ ، يمكننا القول أن الحل صحيح. هل أنت متأكد من هذا؟ ربما هناك أداة من شأنها أن تعطي ردود فعل أكثر دقة؟
اختبار طفرة
يصف هذا المصطلح موقفًا حيث نقوم بتعديل أجزاء صغيرة من التعليمات البرمجية ونرى كيف يؤثر ذلك على الاختبارات. إذا أجريت الاختبارات بعد إجراء التغييرات بشكل صحيح ، فهذا يشير إلى أن الاختبارات لا تكفي لهذه الأجزاء من الكود. بالطبع ، كل هذا يتوقف على ما نغيره بالضبط ، حيث أننا لسنا بحاجة لاختبار جميع أصغر التغييرات ، على سبيل المثال ، المسافات البادئة أو أسماء المتغيرات ، لأنه بعدها يجب أن تكتمل الاختبارات بشكل صحيح. لذلك ، في اختبارات التحوّل ، نستخدم ما يسمى mutators (أساليب التعديل) ، التي تحل محل قطعة من الشفرة بآخر ، ولكن بطريقة منطقية. سنتحدث عن هذا بمزيد من التفصيل أدناه. في بعض الأحيان ، نجري مثل هذه الاختبارات بأنفسنا ، ونتحقق مما إذا كانت الاختبارات تنكسر إذا قمنا بتغيير شيء ما في الكود. إذا قمنا بإعادة تشكيل "نصف النظام" وكانت الاختبارات لا تزال خضراء ، فيمكننا القول على الفور إنها سيئة. وإذا فعل شخص ما هذا وكانت الاختبارات جيدة ، فتهانينا!
الإطار المعدية
اليوم في PHP ، إطار اختبار الطفرة الأكثر شعبية هو
العدوى . وهو يدعم PHPUnit و PHPSpec ، ويتطلب PHP 7.1+ و Xdebug أو phpdbg للعمل معها.
الإطلاق الأول والتكوين
في البداية ، نرى التكوين التفاعلي للإطار ، الذي ينشئ ملفًا خاصًا به الإعدادات - infection.json.dist. يبدو شيء مثل هذا:
{ "timeout": 10, "source": { "directories": [ "src" }, "logs": { "text": "infection.log", "perMutator": "per-mutator.md" }, "mutators": { "@default": true }
Timeout
- خيار يجب أن تكون قيمته مساوية للحد الأقصى لمدة اختبار واحد. في
source
نحدد الدلائل التي سنقوم بتحويل الكود منها ، يمكنك تعيين استثناءات. في
logs
يوجد خيار
text
، نقوم بتعيينه لجمع إحصائيات حول الاختبارات الخاطئة فقط ، وهو الأكثر إثارة للاهتمام بالنسبة لنا. يسمح لك الخيار
perMutator
بحفظ الطفرات المستخدمة. اقرأ المزيد عن هذا في الوثائق.
مثال
final class Calculator { public function add(int $a, int $b): int { return $a + $b; } }
دعنا نقول لدينا الطبقة أعلاه. دعنا نكتب اختبار في PHPUnit:
final class CalculatorTest extends TestCase { private $calculator; public function setUp(): void { $this->calculator = new Calculator(); } public function testAdd(int $a, int $b, int $expected): void { $this->assertEquals($expected, $this->calculator->add($a, $b)); } public function additionProvider(): array { return [ [0, 0, 0], [6, 4, 10], [-1, -2, -3], [-2, 2, 0] ]; } }
بالطبع ، يجب كتابة هذا الاختبار قبل تطبيق طريقة
add()
. عند التنفيذ
./vendor/bin/phpunit
نحصل على:
PHPUnit 8.2.2 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 39 ms, Memory: 4.00 MB OK (4 tests, 4 assertions)
الآن قم بتشغيل
./vendor/bin/infection
:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
وفقا للعدوى ، اختباراتنا دقيقة. في ملف
per -mutator.md ، يمكننا أن نرى ما هي الطفرات التي استخدمت:
يعد Mutator Plus تغييرًا بسيطًا للعلامة من علامة الجمع إلى علامة الطرح ، والتي يجب أن تنهي الاختبارات.
PublicVisibility
PublicVisibility معدل وصول هذه الطريقة ، والذي يجب أن يكسر الاختبارات ، وفي هذه الحالة يعمل.
الآن دعونا نضيف طريقة أكثر تعقيدا.
public function findGreaterThan(array $numbers, int $threshold): array { return \array_values(\array_filter($numbers, static function (int $number) use ($threshold) { return $number > $threshold; })); } public function testFindGreaterThan(array $numbers, int $threshold, array $expected): void { $this->assertEquals($expected, $this->calculator->findGreaterThan($numbers, $threshold)); } public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []] ]; }
بعد التنفيذ ، سنرى النتيجة التالية:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
اختباراتنا ليست على ما يرام. أولاً ، تحقق من ملف infection.log:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:19 [M] UnwrapArrayValues --- Original +++ New @@ @@ */ public function findGreaterThan(array $numbers, int $threshold) : array { - return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { + return \array_filter($numbers, static function (int $number) use($threshold) { return $number > $threshold; - })); + }); } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:20 [M] GreaterThan --- Original +++ New @@ @@ public function findGreaterThan(array $numbers, int $threshold) : array { return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { - return $number > $threshold; + return $number >= $threshold; })); } Timed Out mutants: ================== Not Covered mutants: ====================
أول مشكلة لم يتم اكتشافها هي استخدام دالة
array_values
. يتم استخدامه لإعادة تعيين المفاتيح ، لأن
array_filter
بإرجاع القيم مع المفاتيح من الصفيف السابق. بالإضافة إلى ذلك ، لا توجد حالة في اختباراتنا عندما يكون مطلوبًا استخدام
array_values
، حيث يتم إرجاع صفيف بنفس القيم ولكن يتم إرجاع مفاتيح مختلفة.
المشكلة الثانية تتعلق بحالات الحدود. في المقابل ، استخدمنا العلامة
>
، لكننا لا نختبر أي حالات حدودية ، لذا فإن الاستبدال بـ
>=
لا يكسر الاختبارات. تحتاج إلى إضافة اختبار واحد فقط:
public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []], [[4, 5, 6], 4, [5, 6]] ]; }
والآن العدوى سعيدة بكل شيء:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
أضف طريقة
subtract
إلى فئة
Calculator
، ولكن دون اختبار منفصل في PHPUnit:
public function subtract(int $a, int $b): int { return $a - $b; }
وبعد تشغيل العدوى نرى:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
هذه المرة ، أعادت الأداة طفرات غير مكتشفة.
Escaped mutants: ================ Timed Out mutants: ================== Not Covered mutants: ==================== 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:24 [M] PublicVisibility --- Original +++ New @@ @@ return $number > $threshold; })); } - public function subtract(int $a, int $b) : int + protected function subtract(int $a, int $b) : int { return $a - $b; } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Minus --- Original +++ New @@ @@ public function subtract(int $a, int $b) : int { - return $a - $b; + return $a + $b; }
المقاييس
بعد كل عملية تنفيذ ، تُرجع الأداة ثلاثة مقاييس:
Metrics: Mutation Score Indicator (MSI): 47% Mutation Code Coverage: 67% Covered Code MSI: 70%
Mutation Score Indicator
- النسبة المئوية للطفرات التي اكتشفتها الاختبارات.
يتم حساب القياس على النحو التالي:
TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; MSI = (TotalDefeatedMutants / TotalMutantsCount) * 100;
Mutation Code Coverage
- نسبة الشفرة التي تغطيها الطفرات.
يتم حساب القياس على النحو التالي:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; CoveredRate = (TotalCoveredByTestsMutants / TotalMutantsCount) * 100;
Covered Code Mutation Score Indicator
تحوّل
Covered Code Mutation Score Indicator
- يحدد فعالية الاختبارات للشفرة التي تغطيها الاختبارات فقط.
يتم حساب القياس على النحو التالي:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; CoveredCodeMSI = (TotalDefeatedMutants / TotalCoveredByTestsMutants) * 100;
استخدام في مشاريع أكثر تعقيدا
في المثال أعلاه ، هناك فئة واحدة فقط ، لذلك قمنا بتشغيل العدوى بدون معلمات. ولكن في العمل اليومي في المشاريع العادية ، سيكون من المفيد استخدام المعلمة
–filter
، والتي تتيح لك تحديد مجموعة الملفات التي نريد تطبيق الطفرات عليها.
./vendor/bin/infection --filter=Calculator.php
ايجابيات كاذبة
لا تؤثر بعض الطفرات على أداء الكود ، وتُرجع العدوى MSI إلى أقل من 100٪. لكن لا يمكننا دائمًا القيام بشيء ما مع هذا ، لذلك يتعين علينا التعامل مع مثل هذه الحالات. يظهر شيء مشابه في هذا المثال:
public function calcNumber(int $a): int { return $a / $this->getRatio(); } private function getRatio(): int { return 1; }
بالطبع ، هنا طريقة
getRatio
معنى لها ، في مشروع عادي ، قد يكون هناك نوع من الحساب بدلاً من ذلك. لكن النتيجة يمكن أن تكون
1
. عودة العدوى:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Division --- Original +++ New @@ @@ public function calcNumber(int $a) : int { - return $a / $this->getRatio(); + return $a * $this->getRatio(); } private function getRatio() : int
كما نعلم ، فإن الضرب والقسمة على 1 يؤدي إلى إرجاع نفس النتيجة ، مساوية للرقم الأصلي. لذلك لا ينبغي لهذا التحور أن يكسر الاختبارات ، وعلى الرغم من تشريح العدوى فيما يتعلق بدقة اختباراتنا ، فكل شيء على ما يرام.
الأمثل للمشاريع الكبيرة
في الحالات ذات المشروعات الكبيرة ، يمكن أن تستغرق العدوى الكثير من الوقت. يمكنك تحسين التنفيذ خلال CI إذا كنت تعالج الملفات المعدلة فقط. لمزيد من المعلومات ، راجع الوثائق:
https://infection.imtqy.com/guide/how-to.htmlبالإضافة إلى ذلك ، يمكنك إجراء اختبارات على الكود المعدل بالتوازي. ومع ذلك ، هذا ممكن فقط إذا كانت جميع الاختبارات مستقلة. وهي ، ينبغي أن يكون هذا اختبارات جيدة. لتمكين هذا الخيار ، استخدم
–threads
:
./vendor/bin/infection --threads=4
كيف يعمل؟
يستخدم إطار عمل العدوى AST (شجرة Syntax Abstract) ، والتي تمثل الكود بمثابة بنية بيانات مجردة. لهذا ، يتم استخدام محلل مكتوب بواسطة أحد منشئي PHP (
محلل php ).
يمكن تمثيل العملية المبسطة للأداة على النحو التالي:
- جيل على أساس AST جيل.
- استخدام الطفرات المناسبة (القائمة الكاملة هنا ).
- إنشاء رمز تعديل AST-.
- قم بإجراء الاختبارات فيما يتعلق بالرمز الذي تم تغييره.
على سبيل المثال ، يمكنك التحقق من ناقص استبدال المحول زائد:
<?php declare(strict_types=1); namespace Infection\Mutator\Arithmetic; use Infection\Mutator\Util\Mutator; use PhpParser\Node; use PhpParser\Node\Expr\Array_; final class Plus extends Mutator { public function mutate(Node $node) { return new Node\Expr\BinaryOp\Minus($node->left, $node->right, $node->getAttributes()); } protected function mutatesNode(Node $node): bool { if (!($node instanceof Node\Expr\BinaryOp\Plus)) { return false; } if ($node->left instanceof Array_ || $node->right instanceof Array_) { return false; } return true; } }
ينشئ الأسلوب
mutate()
عنصرًا جديدًا ، يتم استبداله بعلامة الجمع. يتم أخذ فئة
Node
من حزمة محلل php ؛ يتم استخدامها لعمليات AST ولتعديل كود PHP. ومع ذلك ، لا يمكن تطبيق هذا التغيير في أي مكان ، لذلك يحتوي الأسلوب
mutatesNode()
على شروط إضافية. إذا كان يوجد صفيف على يسار علامة الجمع أو إلى يمين الطرح ، فلن يُسمح بالتغيير. يتم استخدام هذا الشرط بسبب هذا الرمز:
$tab = [0] + [1]; is correct, but the following one isn't correct. $tab = [0] - [1];
يؤدي
يعد اختبار التحور أداة ممتازة تكمل عملية CI وتتيح لك تقييم جودة الاختبارات. تسليط الضوء الأخضر من الاختبارات لا يمنحنا الثقة في أن كل شيء مكتوب بشكل جيد. يمكنك تحسين دقة الاختبارات باستخدام الاختبار التحولي - أو اختبار الاختبار - مما يزيد من ثقتنا في أداء الحل. بالطبع ، ليس من الضروري السعي لتحقيق 100 ٪ من نتائج المقاييس ، لأن هذا ليس ممكنًا دائمًا. تحتاج إلى تحليل السجلات وتكوين الاختبارات وفقا لذلك.