فهم تغطية الكود
تغطية الكود هي مقياس يقيس النسبة المئوية من قاعدة الكود الخاصة بك التي يتم تنفيذها عند تشغيل الاختبارات الآلية. تساعدك في تحديد الأجزاء غير المختبرة من الكود وتقييم شمولية مجموعة الاختبارات. بينما لا تضمن التغطية بنسبة 100% كوداً خالياً من الأخطاء، فإنها توفر رؤى قيمة حول جهود الاختبار.
لماذا تهم تغطية الكود
تخدم تغطية الكود عدة أغراض مهمة في تطوير البرمجيات:
- تحديد الكود غير المختبر: تسليط الضوء على مناطق قاعدة الكود التي تفتقر إلى تغطية الاختبار
- قياس اكتمال الاختبارات: توفير بيانات كمية حول شمولية مجموعة الاختبارات
- توجيه جهود الاختبار: المساعدة في تحديد أولويات المناطق التي تحتاج إلى مزيد من الاختبار
- منع الانحدارات: التأكد من اختبار المسارات الحرجة قبل النشر
- تحسين جودة الكود: تشجيع كتابة كود قابل للاختبار ومعياري
مهم: تغطية الكود العالية هي مؤشر جيد ولكنها ليست ضماناً للجودة. يمكن أن يكون لديك تغطية 100% باختبارات ضعيفة لا تتحقق فعلياً من السلوك. ركز على التأكيدات المفيدة، وليس فقط تنفيذ الأسطر.
أنواع تغطية الكود
هناك عدة أنواع من مقاييس التغطية، كل منها يقيس جوانب مختلفة من تنفيذ الكود:
1. تغطية الأسطر (تغطية البيانات)
تقيس النسبة المئوية من أسطر الكود التي يتم تنفيذها أثناء الاختبارات:
function calculateDiscount(price, quantity) {
let discount = 0; // السطر 1 - مغطى
if (quantity > 10) { // السطر 2 - مغطى
discount = 0.1; // السطر 3 - غير مغطى إذا كان quantity <= 10
}
return price * discount; // السطر 4 - مغطى
}
// الاختبار يغطي فقط الأسطر 1، 2، 4 (75% تغطية أسطر)
test('no discount for small orders', () => {
expect(calculateDiscount(100, 5)).toBe(0);
});
2. تغطية الفروع (تغطية القرارات)
تقيس ما إذا كان قد تم تنفيذ كل فرع من البيانات الشرطية:
function validateAge(age) {
if (age > 18) { // نقطة التفرع
return 'adult'; // الفرع الصحيح
} else {
return 'minor'; // الفرع الخاطئ
}
}
// نحتاج اختبارات لكلا الفرعين (100% تغطية فروع)
test('adult age', () => {
expect(validateAge(25)).toBe('adult');
});
test('minor age', () => {
expect(validateAge(15)).toBe('minor');
});
3. تغطية الدوال
تقيس النسبة المئوية من الدوال المعرفة التي يتم استدعاؤها:
class Calculator {
add(a, b) { return a + b; } // مستدعى في الاختبارات
subtract(a, b) { return a - b; } // مستدعى في الاختبارات
multiply(a, b) { return a * b; } // غير مستدعى
divide(a, b) { return a / b; } // غير مستدعى
}
// فقط 50% تغطية دوال (2 من 4 دوال مختبرة)
4. تغطية الشروط
تقيس ما إذا كانت كل تعبير منطقي فرعي قد تم تقييمه إلى true و false:
function isEligible(age, hasLicense) {
if (age >= 18 && hasLicense) { // شرطان
return true;
}
return false;
}
// تغطية الشروط الكاملة تتطلب اختبار:
// age >= 18 (true/false) و hasLicense (true/false)
test('eligible with both conditions true', () => {
expect(isEligible(20, true)).toBe(true);
});
test('not eligible - young with license', () => {
expect(isEligible(16, true)).toBe(false);
});
test('not eligible - adult without license', () => {
expect(isEligible(25, false)).toBe(false);
});
تغطية كود JavaScript مع Istanbul/nyc
Istanbul (يُسمى الآن nyc) هو أداة تغطية الكود الأكثر شيوعاً لـ JavaScript. يتكامل بسلاسة مع Jest و Mocha وأطر اختبار أخرى:
إعداد nyc مع Mocha
# تثبيت nyc
npm install --save-dev nyc
# package.json
{
"scripts": {
"test": "mocha",
"test:coverage": "nyc mocha"
}
}
# تشغيل الاختبارات مع التغطية
npm run test:coverage
تكوين nyc (.nycrc.json)
{
"all": true,
"include": ["src/**/*.js"],
"exclude": [
"**/*.spec.js",
"**/*.test.js",
"**/node_modules/**",
"**/tests/**"
],
"reporter": ["text", "html", "lcov"],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
نصائح التكوين:
- استخدم
"all": true لتضمين الملفات غير المختبرة في تقرير التغطية
- استبعد ملفات الاختبار وملفات التكوين والكود الخارجي
- حدد حدود تغطية واقعية تزداد بمرور الوقت
- استخدم مُبلغين متعددين: text للطرفية، html للعرض التفصيلي
تغطية Jest المدمجة
Jest لديه Istanbul مدمج، مما يجعل تحليل التغطية بسيطاً للغاية:
# تشغيل Jest مع التغطية
npm test -- --coverage
# أو أضف إلى package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage --watchAll=false"
}
}
# jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js'
],
coverageThreshold: {
global: {
branches: 75,
functions: 75,
lines: 75,
statements: 75
}
}
};
تغطية كود PHP مع PHPUnit
توفر PHPUnit تحليل تغطية كود شامل باستخدام Xdebug أو phpdbg:
المتطلبات الأساسية
# تثبيت Xdebug (الخيار 1)
pecl install xdebug
# أو استخدام phpdbg (الخيار 2، مضمن مع PHP)
phpdbg -qrr vendor/bin/phpunit --coverage-html coverage
# التحقق من تمكين Xdebug
php -v | grep Xdebug
تكوين PHPUnit (phpunit.xml)
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory suffix=".php">./app/Console</directory>
<file>./app/Http/Kernel.php</file>
</exclude>
<report>
<html outputDirectory="./coverage/html"/>
<clover outputFile="./coverage/clover.xml"/>
<text outputFile="php://stdout" showUncoveredFiles="true"/>
</report>
</coverage>
</phpunit>
تشغيل PHPUnit مع التغطية
# إنشاء تقرير تغطية HTML
./vendor/bin/phpunit --coverage-html coverage
# إنشاء تقرير تغطية نصي
./vendor/bin/phpunit --coverage-text
# إنشاء تنسيقات متعددة
./vendor/bin/phpunit --coverage-html coverage --coverage-clover coverage.xml
# خاص بـ Laravel
php artisan test --coverage
php artisan test --coverage --min=80
فهم تقارير التغطية
توفر تقارير التغطية رؤى تفصيلية حول تغطية الاختبار الخاصة بك. إليك كيفية تفسيرها:
تقرير نصي في الطرفية
تقرير تغطية الكود:
2024-02-14 10:30:00
الملخص:
الفئات: 85.71% (6/7)
الطرق: 78.26% (18/23)
الأسطر: 82.35% (140/170)
App\Services\PaymentService
الطرق: 100.00% (5/5)
الأسطر: 95.00% (19/20)
App\Controllers\OrderController
الطرق: 66.67% (4/6)
الأسطر: 75.00% (45/60)
تقرير تغطية HTML
توفر تقارير HTML عروضاً تفاعلية مُرمزة بالألوان للكود الخاص بك:
- الأسطر الخضراء: مغطاة بالاختبارات
- الأسطر الحمراء: غير مغطاة بالاختبارات
- الأسطر البرتقالية: مغطاة جزئياً (بعض الفروع)
- الأسطر الرمادية: غير قابلة للتنفيذ (تعليقات، تصريحات)
أفضل ممارسة: احتفظ بتقارير تغطية HTML خارج نظام التحكم في الإصدارات. أضف coverage/ إلى ملف .gitignore الخاص بك. هذه التقارير هي مُنتجات يتم إنشاؤها محلياً أو في خطوط CI/CD.
تعيين حدود التغطية
تفشل حدود التغطية تلقائياً البناءات إذا انخفضت التغطية عن المستويات المحددة:
حدود Jest
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 85,
lines: 85,
statements: 85
},
'./src/services/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
},
'./src/utils/formatting.js': {
branches: 100,
functions: 100,
lines: 100,
statements: 100
}
}
};
فحص تغطية PHPUnit
<!-- phpunit.xml -->
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
<report>
<html outputDirectory="./coverage"/>
</report>
</coverage>
<!-- طلب حد أدنى للتغطية (PHPUnit 9.3+) -->
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
</source>
# فحص التغطية من سطر الأوامر
phpunit --coverage-text --coverage-filter app/Services
# فشل البناء إذا كانت التغطية أقل من الحد (CI/CD)
phpunit --coverage-text | grep -E "^\s+Lines:\s+([0-9]+\.[0-9]+)%" | \
awk '{if ($2 < 80.00) exit 1}'
التغطية في خطوط CI/CD
دمج فحوصات التغطية في سير عمل التكامل المستمر:
مثال GitHub Actions
name: Tests with Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
fail_ci_if_error: true
أفضل الممارسات لتغطية الكود
1. لا تسعى بشكل أعمى للحصول على تغطية 100%
ركز على تغطية المنطق التجاري الحرج، وليس الكود الروتيني. بعض الكود (مثل getters/setters البسيطة، ملفات التكوين) لا يحتاج إلى اختبارات.
2. استخدم التغطية لإيجاد الثغرات، وليس كهدف
التغطية هي أداة لتحديد الكود غير المختبر، وليست مقياساً للتلاعب. اكتب اختبارات للسلوك، وليس لأرقام التغطية.
3. ادمج مع اختبار الطفرات
أدوات مثل Stryker (JavaScript) و Infection (PHP) تتحقق من أن اختباراتك تلتقط الأخطاء فعلياً، وليس فقط تنفيذ الأسطر.
4. حدد حدوداً واقعية ومتزايدة
ابدأ بالتغطية الحالية وزد الحدود تدريجياً. لا تحدد تغطية 90% على مشروع بتغطية 40%.
5. استبعد الملفات الصحيحة
لا تضمن ملفات الاختبار أو الترحيلات أو التكوين أو الكود المُولد في حسابات التغطية.
المزالق الشائعة:
- مسرح التغطية: كتابة اختبارات تنفذ الكود بدون تأكيدات مفيدة
- تجاهل تغطية الفروع: التركيز فقط على تغطية الأسطر يفوت مسارات شرطية غير مختبرة
- اختبار تفاصيل التنفيذ: تغطية عالية على دوال داخلية يمكن أن تتغير
- إهمال الحالات الحدية: تغطية المسارات السعيدة لكن تفويت معالجة الأخطاء
- ثقة زائفة: افتراض أن تغطية 100% تعني كود خالي من الأخطاء
تمرين عملي
التمرين 1: تحليل التغطية لوحدة سلة التسوق
// shopping-cart.js
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(product, quantity = 1) {
if (quantity <= 0) {
throw new Error('Quantity must be positive');
}
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
removeItem(productId) {
const index = this.items.findIndex(item => item.product.id === productId);
if (index !== -1) {
this.items.splice(index, 1);
}
}
getTotal() {
return this.items.reduce((sum, item) => {
return sum + (item.product.price * item.quantity);
}, 0);
}
applyDiscount(percentage) {
if (percentage < 0 || percentage > 100) {
throw new Error('Invalid discount percentage');
}
const total = this.getTotal();
return total * (1 - percentage / 100);
}
}
// اكتب اختبارات لتحقيق:
// - 100% تغطية أسطر
// - 100% تغطية فروع
// - 100% تغطية دوال
// تشغيل: jest --coverage
التمرين 2: تحسين التغطية لفئة مُدقق PHP
// app/Services/EmailValidator.php
class EmailValidator
{
public function validate(string $email): bool
{
if (empty($email)) {
return false;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
[$local, $domain] = explode('@', $email);
if (strlen($local) > 64) {
return false;
}
if (!checkdnsrr($domain, 'MX')) {
return false;
}
return true;
}
}
// تشغيل: phpunit --coverage-html coverage
// حدد الفروع غير المختبرة
// اكتب اختبارات للوصول إلى تغطية 100%
التمرين 3: إعداد حدود التغطية لمشروع
- قم بتشغيل تحليل التغطية على مشروعك الحالي
- حدد نسب التغطية الحالية
- عيّن حدود 5-10% أعلى من التغطية الحالية
- قم بتكوين CI/CD للفشل عند انتهاك الحدود
- اكتب اختبارات للوفاء بالحدود الجديدة