اختبار الطفرات
يقيّم اختبار الطفرات جودة مجموعة الاختبارات الخاصة بك من خلال إدخال أخطاء مقصودة (طفرات) في الكود الخاص بك والتحقق مما إذا كانت اختباراتك تكتشفها. في هذا الدرس، سنستكشف مفاهيم اختبار الطفرات وأدوات مثل Infection PHP وStryker JS واستراتيجيات لتحسين فعالية الاختبار.
ما هو اختبار الطفرات؟
يجيب اختبار الطفرات على سؤال حاسم: "هل اختباراتي تختبر بالفعل أي شيء ذي معنى؟"
يخبرك تغطية الكود التقليدية عن الأسطر التي يتم تنفيذها أثناء الاختبارات، لكنها لا تخبرك ما إذا كانت تلك الاختبارات ستكتشف الأخطاء. يمكن أن يكون لديك تغطية كود 100% مع تأكيدات لا تتحقق أبدًا من السلوك الصحيح.
يعمل اختبار الطفرات من خلال:
- إنشاء الطفرات: تعديل الكود المصدري تلقائيًا بطرق صغيرة (مثل تغيير
+ إلى -، > إلى >=، إزالة استدعاءات الطرق)
- تشغيل الاختبارات: تنفيذ مجموعة الاختبارات ضد كل طفرة
- التسجيل: يتم "قتل" الطفرة إذا فشلت الاختبارات، "نجت" إذا نجحت الاختبارات (مما يشير إلى اختبارات ضعيفة)
- قياس الجودة: درجة الطفرات = (الطفرات المقتولة / إجمالي الطفرات) × 100%
رؤية أساسية: تشير الدرجة العالية للطفرات إلى أن اختباراتك من المحتمل أن تكتشف أخطاء حقيقية. تعني الدرجة المنخفضة للطفرات أن لديك اختبارات ضعيفة تعطي ثقة زائفة.
أنواع الطفرات
تُدخل عوامل الطفرات الشائعة أنواعًا مختلفة من التغييرات:
- العمليات الحسابية:
+ → -، * → /، % → *
- العمليات العلائقية:
> → >=، == → !=، < → <=
- العمليات المنطقية:
&& → ||، إزالة !
- القيم المُرجعة:
return true → return false، return $value → return null
- استدعاءات الطرق: إزالة استدعاءات الطرق، تغيير الوسائط
- الشرطيات: إزالة شروط if، عكس الشروط
- الزيادات:
++ → --، $i++ → $i
اختبار الطفرات باستخدام Infection PHP
Infection هو إطار عمل اختبار الطفرات الرائد لـ PHP:
# تثبيت Infection
composer require --dev infection/infection
# تهيئة التكوين
vendor/bin/infection --configure
# infection.json5 (التكوين المُنشأ)
{
"$schema": "vendor/infection/infection/resources/schema.json",
"source": {
"directories": ["src"]
},
"timeout": 10,
"logs": {
"text": "infection.log",
"html": "infection-report.html"
},
"mutators": {
"@default": true
},
"minMsi": 80, // مؤشر درجة الطفرات الأدنى
"minCoveredMsi": 90 // MSI الأدنى للكود المغطى
}
# تشغيل Infection
vendor/bin/infection
# التشغيل مع مُحوِّلات محددة
vendor/bin/infection --mutators=@arithmetic,@conditional
# التشغيل على ملفات محددة
vendor/bin/infection --filter=src/Services/Calculator.php
# التشغيل بخيوط متعددة (أسرع)
vendor/bin/infection --threads=4
مثال: اختبارات ضعيفة تم الكشف عنها باختبار الطفرات
ضع في اعتبارك هذا الكود واختباره:
<?php
// src/Services/PriceCalculator.php
class PriceCalculator
{
public function calculateDiscount(float $price, float $discountPercent): float
{
if ($discountPercent > 100) {
throw new InvalidArgumentException('لا يمكن أن يتجاوز الخصم 100%');
}
$discount = $price * ($discountPercent / 100);
return $price - $discount;
}
public function applyTax(float $amount, float $taxRate): float
{
return $amount + ($amount * $taxRate);
}
}
// tests/Services/PriceCalculatorTest.php
class PriceCalculatorTest extends TestCase
{
// اختبار ضعيف - يتحقق فقط من المسار السعيد
public function test_calculate_discount()
{
$calculator = new PriceCalculator();
$result = $calculator->calculateDiscount(100, 10);
$this->assertEquals(90, $result);
}
// اختبار ضعيف - لا يتحقق من الحساب
public function test_apply_tax()
{
$calculator = new PriceCalculator();
$result = $calculator->applyTax(100, 0.1);
$this->assertIsFloat($result); // سيء - يتحقق فقط من النوع!
}
}
قم بتشغيل Infection على هذا الكود:
# ناتج Infection:
مؤشر درجة الطفرات (MSI): 30%
MSI الكود المغطى: 40%
الطفرات الناجية:
1) src/Services/PriceCalculator.php:9
- تم تغيير > إلى >=
- نجت الطفرة (لم يكتشفها الاختبار)
2) src/Services/PriceCalculator.php:13
- تم تغيير - إلى +
- نجت الطفرة (لم يكتشفها الاختبار)
3) src/Services/PriceCalculator.php:18
- تم تغيير + إلى -
- نجت الطفرة (لم يكتشفها الاختبار)
4) src/Services/PriceCalculator.php:18
- تم تغيير * إلى /
- نجت الطفرة (لم يكتشفها الاختبار)
تحسين الاختبارات بناءً على نتائج الطفرات
قم بإصلاح الاختبارات الضعيفة لقتل الطفرات:
<?php
class PriceCalculatorTest extends TestCase
{
// محسّن - يختبر شروط الحدود
public function test_calculate_discount()
{
$calculator = new PriceCalculator();
// اختبار الخصم العادي
$this->assertEquals(90, $calculator->calculateDiscount(100, 10));
// اختبار عدم وجود خصم
$this->assertEquals(100, $calculator->calculateDiscount(100, 0));
// اختبار الخصم الأقصى
$this->assertEquals(0, $calculator->calculateDiscount(100, 100));
// اختبار الحد: 100% يجب أن يعمل
$this->assertEquals(0, $calculator->calculateDiscount(100, 100));
}
// محسّن - يختبر الإدخال غير الصالح
public function test_calculate_discount_throws_on_invalid_percent()
{
$calculator = new PriceCalculator();
$this->expectException(InvalidArgumentException::class);
$calculator->calculateDiscount(100, 101);
}
// محسّن - يتحقق من الحساب الفعلي
public function test_apply_tax()
{
$calculator = new PriceCalculator();
// اختبار ضريبة 10%
$this->assertEquals(110, $calculator->applyTax(100, 0.1));
// اختبار ضريبة 0%
$this->assertEquals(100, $calculator->applyTax(100, 0));
// اختبار ضريبة 25%
$this->assertEquals(125, $calculator->applyTax(100, 0.25));
// اختبار مع مبالغ عشرية
$this->assertEquals(110.55, $calculator->applyTax(99.59, 0.11), '', 0.01);
}
}
# قم بتشغيل Infection مرة أخرى:
مؤشر درجة الطفرات (MSI): 95%
MSI الكود المغطى: 98%
تم قتل جميع الطفرات! ✓
أفضل ممارسة: اهدف إلى درجة طفرات من 80-90%. لا تهوس بـ 100%—بعض الطفرات متكافئة (تنتج نفس السلوك) أو تمثل سيناريوهات غير واقعية.
اختبار الطفرات باستخدام Stryker JS
Stryker هو إطار عمل اختبار الطفرات لـ JavaScript/TypeScript:
# تثبيت Stryker
npm install --save-dev @stryker-mutator/core
npm install --save-dev @stryker-mutator/jest-runner
# تهيئة التكوين
npx stryker init
# stryker.conf.json (مُنشأ)
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"packageManager": "npm",
"reporters": ["html", "clear-text", "progress", "dashboard"],
"testRunner": "jest",
"coverageAnalysis": "perTest",
"mutate": [
"src/**/*.js",
"!src/**/*.test.js"
],
"thresholds": {
"high": 80,
"low": 60,
"break": 50
}
}
# تشغيل Stryker
npx stryker run
# مثال على كود JavaScript واختباراته
// src/calculator.js
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('القسمة على صفر');
}
return a / b;
}
// src/calculator.test.js (اختبار ضعيف)
import { add, divide } from './calculator';
test('إضافة أرقام', () => {
expect(add(2, 3)).toBe(5);
});
test('قسمة أرقام', () => {
const result = divide(10, 2);
expect(result).toBeDefined(); // سيء - تأكيد ضعيف
});
# ناتج Stryker:
نجت الطفرة: تم تغيير + إلى - في ()add
نجت الطفرة: تم تغيير / إلى * في ()divide
نجت الطفرة: تم تغيير === إلى !== في ()divide
درجة الطفرات: 33%
تحسين اختبارات JavaScript
اقتل الطفرات بتأكيدات أفضل:
// src/calculator.test.js (محسّن)
import { add, divide } from './calculator';
describe('add', () => {
test('إضافة أرقام موجبة', () => {
expect(add(2, 3)).toBe(5);
});
test('إضافة أرقام سالبة', () => {
expect(add(-2, -3)).toBe(-5);
});
test('إضافة صفر', () => {
expect(add(5, 0)).toBe(5);
expect(add(0, 5)).toBe(5);
});
});
describe('divide', () => {
test('قسمة أرقام موجبة', () => {
expect(divide(10, 2)).toBe(5);
});
test('قسمة مع كسور عشرية', () => {
expect(divide(10, 3)).toBeCloseTo(3.33, 2);
});
test('رمي خطأ عند القسمة على صفر', () => {
expect(() => divide(10, 0)).toThrow('القسمة على صفر');
});
test('قسمة أرقام سالبة', () => {
expect(divide(-10, 2)).toBe(-5);
expect(divide(10, -2)).toBe(-5);
});
});
# ناتج Stryker:
درجة الطفرات: 92%
تم قتل جميع الطفرات الحرجة! ✓
أنماط اختبار الطفرات الشائعة
تعلم كيفية التعرف على أنماط الطفرات الشائعة وقتلها:
// النمط 1: شروط الحدود
function isAdult(age) {
return age >= 18; // طفرة: >= → >
}
// القتل باختبار الحدود
test('الحد عند 18', () => {
expect(isAdult(17)).toBe(false);
expect(isAdult(18)).toBe(true); // حرج!
expect(isAdult(19)).toBe(true);
});
// النمط 2: إرجاع قيم منطقية
function isValid(value) {
if (value) {
return true; // طفرة: true → false
}
return false; // طفرة: false → true
}
// القتل بتحققات صريحة
test('التحقق بشكل صحيح', () => {
expect(isValid('test')).toBe(true);
expect(isValid('')).toBe(false);
expect(isValid(null)).toBe(false);
});
// النمط 3: العمليات الحسابية
function calculateTotal(price, quantity) {
return price * quantity; // طفرة: * → +, -, /
}
// القتل بقيم محددة
test('حساب الإجمالي', () => {
expect(calculateTotal(10, 3)).toBe(30); // ليس 13، 7، أو 3.33
expect(calculateTotal(5, 0)).toBe(0);
expect(calculateTotal(0, 10)).toBe(0);
});
// النمط 4: إزالة استدعاء الطريقة
function processUser(user) {
validateUser(user); // طفرة: إزالة هذا الاستدعاء
saveUser(user); // طفرة: إزالة هذا الاستدعاء
return user;
}
// القتل بالتحقق من الآثار الجانبية
test('معالجة المستخدم بالكامل', () => {
const validateSpy = jest.spyOn(module, 'validateUser');
const saveSpy = jest.spyOn(module, 'saveUser');
processUser({ name: 'John' });
expect(validateSpy).toHaveBeenCalled();
expect(saveSpy).toHaveBeenCalled();
});
// النمط 5: عمليات المصفوفات/المجموعات
function filterActive(users) {
return users.filter(u => u.active); // طفرة: active → !active
}
// القتل بحالات إيجابية وسلبية
test('تصفية المستخدمين النشطين', () => {
const users = [
{ id: 1, active: true },
{ id: 2, active: false },
{ id: 3, active: true },
];
const result = filterActive(users);
expect(result).toHaveLength(2);
expect(result[0].id).toBe(1);
expect(result[1].id).toBe(3);
expect(result.find(u => u.id === 2)).toBeUndefined();
});
تفسير تقارير اختبار الطفرات
فهم مقاييس اختبار الطفرات:
# مقاييس تقرير Infection/Stryker
1. مؤشر درجة الطفرات (MSI)
- الصيغة: (مقتول + مهلة) / (إجمالي - متجاهل - غير مغطى) × 100
- الهدف: 80-90%
- يقيس فعالية الاختبار الإجمالية
2. MSI الكود المغطى
- الصيغة: (مقتول + مهلة) / (إجمالي - متجاهل) × 100
- الهدف: 90-95%
- يأخذ في الاعتبار فقط الكود المغطى بالاختبارات
3. حالات الطفرات:
- مقتول: فشل الاختبار (جيد - تم اكتشاف الطفرة)
- نجا: نجح الاختبار (سيء - اختبار ضعيف)
- مهلة: تسببت الطفرة في حلقة لا نهائية (جيد)
- غير مغطى: لم ينفذ أي اختبار هذا الكود
- متجاهل: مستثنى بالتكوين
- خطأ: تسببت الطفرة في خطأ صرفي/وقت التشغيل
4. بنية تقرير HTML:
- أخضر: تم قتل الطفرة ✓
- أحمر: نجت الطفرة (يحتاج اختبارًا أفضل) ✗
- رمادي: غير مغطى بالاختبارات
- أصفر: مهلة (الطفرة بطيئة جدًا)
# الأولوية للإصلاح:
1. الطفرات الناجية في مسارات الكود الحرجة
2. الطفرات الناجية في المنطق المعقد
3. الكود غير المغطى (زيادة تغطية الكود أولاً)
4. الطفرات الناجية في الكود البسيط (أولوية منخفضة)
تحذير: اختبار الطفرات بطيء. مجموعة اختبارات تعمل في 30 ثانية قد تستغرق 30 دقيقة مع اختبار الطفرات. قم بتشغيله بشكل دوري (إصدارات ليلية، قبل الإصدارات)، وليس على كل التزام.
تكامل CI/CD
ادمج اختبار الطفرات في خط الأنابيب الخاص بك:
# .github/workflows/mutation-tests.yml
name: Mutation Testing
on:
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 0' # أسبوعيًا يوم الأحد في الساعة 2 صباحًا
jobs:
mutation-tests:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: pcov
- name: Install dependencies
run: composer install
- name: Run PHPUnit (لتغطية خط الأساس)
run: vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Run Infection
run: |
vendor/bin/infection \
--threads=4 \
--min-msi=80 \
--min-covered-msi=90 \
--logger-html=infection-report.html \
--skip-initial-tests
- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v3
with:
name: mutation-report
path: infection-report.html
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const log = fs.readFileSync('infection.log', 'utf8');
const msiMatch = log.match(/Mutation Score Indicator \(MSI\): (\d+)%/);
const msi = msiMatch ? msiMatch[1] : 'N/A';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## نتائج اختبار الطفرات\n\nدرجة الطفرات: ${msi}%`
});
# لـ JavaScript مع Stryker
- name: Run Stryker
run: npx stryker run
- name: Upload Stryker report
uses: stryker-mutator/dashboard-reporter@v1
with:
api-key: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
الطفرات المتكافئة
بعض الطفرات "متكافئة"—لا تغير السلوك:
// مثال 1: طفرة متكافئة
function absolute(n) {
return n < 0 ? -n : n;
// طفرة: < 0 → <= 0
// لـ n=0: الأصلي يُرجع 0، الطفرة تُرجع 0 (متكافئ)
}
// مثال 2: طفرة متكافئة
function count(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total++; // طفرة: total++ → ++total
}
return total; // كلاهما ينتج نفس النتيجة
}
// مثال 3: طفرة متكافئة
const MAX_RETRIES = 3;
function retry(fn) {
for (let i = 0; i < MAX_RETRIES; i++) {
// طفرة: i < MAX_RETRIES → i <= MAX_RETRIES
// إذا كان MAX_RETRIES ثابتًا، السلوك لم يتغير
}
}
// كيفية التعامل:
# infection.json5
{
"mutators": {
"@default": true,
"IncrementInteger": {
"ignore": [
"App\Services\EquivalentClass::methodName"
]
}
}
}
تمرين 1: قم بتثبيت Infection PHP وتشغيله على مشروعك. راجع تقرير الطفرات وحدد أهم 5 طفرات ناجية في منطق الأعمال الحرجة. اكتب اختبارات إضافية لقتل تلك الطفرات وتحسين درجة الطفرات الخاصة بك بنسبة 20% على الأقل.
تمرين 2: قم بإعداد Stryker لمشروع JavaScript. ركز على وحدة معقدة واحدة (مثل منطق التحقق، محرك الحساب). حقق درجة طفرات 90%+ من خلال إضافة اختبارات شاملة تغطي شروط الحدود وحالات الحافة وسيناريوهات الأخطاء.
تمرين 3: أنشئ سير عمل CI/CD يُشغّل اختبارات الطفرات أسبوعيًا وعلى طلبات السحب التي تعدل الملفات الحرجة. قم بتكوينه لـ: (1) الفشل إذا انخفض MSI تحت 80%، (2) توليد تقارير HTML، (3) نشر درجة الطفرات في تعليقات PR، (4) تتبع اتجاهات درجة الطفرات بمرور الوقت.
الملخص
في هذا الدرس، غطينا اختبار الطفرات بشكل شامل:
- فهم اختبار الطفرات كمقياس لجودة الاختبار، وليس فقط التغطية
- تعلم عوامل الطفرات الشائعة (حسابية، منطقية، شرطية، إرجاع)
- استخدام Infection PHP لاختبار الطفرات في مشاريع PHP
- استخدام Stryker لاختبار الطفرات في مشاريع JavaScript/TypeScript
- تحديد وإصلاح الاختبارات الضعيفة بناءً على الطفرات الناجية
- التعرف على أنماط الطفرات الشائعة وكيفية قتلها
- تفسير مقاييس اختبار الطفرات (MSI، MSI الكود المغطى)
- التعامل مع الطفرات المتكافئة بشكل مناسب
- دمج اختبار الطفرات في خطوط أنابيب CI/CD
اختبار الطفرات هو الاختبار النهائي لمجموعة الاختبارات الخاصة بك. بينما تخبرك تغطية الكود عن الكود الذي يتم تنفيذه، يخبرك اختبار الطفرات ما إذا كانت اختباراتك ستكتشف الأخطاء فعليًا. استخدمه لتحسين ممارسات الاختبار الخاصة بك باستمرار وتقديم برمجيات أكثر موثوقية.