اختبار تطبيقات Flutter

تغطية الاختبار: القياس والتحسين

16 دقيقة الدرس 11 من 12

تغطية الاختبار: القياس والتحسين

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

إنشاء تقرير تغطية LCOV

قم بتشغيل اختباراتك مع الراية --coverage لجمع بيانات التغطية الخام. يكتب Flutter النتائج في coverage/lcov.info بتنسيق LCOV القياسي:

جمع بيانات التغطية

# تشغيل جميع الاختبارات وجمع التغطية
flutter test --coverage

# تشغيل ملف اختبار محدد مع التغطية
flutter test test/services/cart_service_test.dart --coverage

# موقع المخرجات
# coverage/lcov.info  (يُنشأ تلقائياً)

ملف lcov.info هو تقرير نصي بسيط تفهمه أدوات CI كثيرة وخدمات التغطية (Codecov، Coveralls) ومولدات HTML المحلية.

تصور الأسطر غير المغطاة باستخدام genhtml

genhtml هو جزء من مجموعة LCOV ويحوّل lcov.info إلى تقرير HTML قابل للتصفح حيث تكون الأسطر المغطاة خضراء والأسطر غير المغطاة حمراء:

إنشاء وفتح تقرير تغطية HTML

# تثبيت lcov (macOS)
brew install lcov

# تثبيت lcov (Ubuntu/Debian)
sudo apt-get install lcov

# إنشاء تقرير HTML
genhtml coverage/lcov.info --output-directory coverage/html

# فتح التقرير في المتصفح الافتراضي (macOS)
open coverage/html/index.html

# الفتح على Linux
xdg-open coverage/html/index.html

يعرض تقرير HTML تفصيلاً لكل ملف ودليل، مما يمنحك خريطة دقيقة للدوال والأسطر والفروع التي لم تلمسها أي اختبارات قط.

نصيحة: أضف coverage/ إلى .gitignore حتى لا تلوث تقارير HTML المولّدة مستودعك. أنشئ coverage/lcov.info فقط إذا كان خط أنابيب CI الخاص بك يحتاجه كمنتج.

تصفية الملفات المولّدة

يُنتج توليد كود Dart (مثل json_serializable وfreezed وتعليقات Riverpod التوضيحية) ملفات *.g.dart و*.freezed.dart تضخّم أرقام التغطية وتخلق ضوضاء. أزلها باستخدام lcov --remove قبل توليد تقرير HTML:

إزالة الملفات المولّدة من بيانات التغطية

# إزالة الملفات المولّدة والملفات تحت lib/generated/
lcov --remove coverage/lcov.info \
  '*.g.dart' \
  '*.freezed.dart' \
  '*/generated/*' \
  --output-file coverage/lcov_filtered.info

# بناء HTML من البيانات النظيفة
genhtml coverage/lcov_filtered.info --output-directory coverage/html

تحديد حدود نسبة التغطية

يُنفّذ الحد الأدنى نسبة تغطية مقبولة دنيا ويُفشل خط أنابيب CI الخاص بك عندما تنخفض التغطية دونه. Flutter نفسه لا يملك راية حد مدمجة، لذا تُنفّذ الفرق هذا عبر نصّ shell صغير أو هدف Makefile:

فرض حد تغطية أدنى في CI

#!/usr/bin/env bash
# scripts/check_coverage.sh

set -e

THRESHOLD=80   # الفشل إذا انخفضت التغطية عن 80%

flutter test --coverage

# استخراج نسبة تغطية الأسطر الإجمالية من lcov.info
LINES_HIT=$(grep -E "^LH:" coverage/lcov.info | awk -F: '{sum += $2} END {print sum}')
LINES_FOUND=$(grep -E "^LF:" coverage/lcov.info | awk -F: '{sum += $2} END {print sum}')
COVERAGE=$(echo "scale=2; $LINES_HIT * 100 / $LINES_FOUND" | bc)

echo "Coverage: ${COVERAGE}%  (threshold: ${THRESHOLD}%)"

if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
  echo "FAIL: Coverage ${COVERAGE}% is below the required ${THRESHOLD}%"
  exit 1
fi

echo "PASS: Coverage threshold met."
ملاحظة: تستخدم العديد من الفرق أيضاً Codecov أو Coveralls مع سير عمل GitHub Actions الخاص بها. تضع هذه الخدمات تعليقات على طلبات السحب تتضمن فرق التغطية، مما يجعل الانحدارات مرئية فوراً للمراجعين. قم بتكوينها عن طريق رفع lcov.info كمنتج CI وإضافة إجراء المزود إلى سير عملك.

تحديد مسارات الكود التي تحتاج إلى اختبارات

بمجرد فتح تقرير HTML، ركز أولاً على الملفات ذات أدنى نسب تغطية. داخل الملف، ابحث عن:

  • الأسطر الحمراء — عبارات لم يتم الوصول إليها قط أثناء تشغيل الاختبار
  • علامات الفروع — الأماكن الحمراء نصف المعيّن تشير إلى فرع if/else اختُبر جزئياً فقط
  • الدوال غير المغطاة — طرق بأكملها لم يستدعِها أي اختبار
  • مسارات معالجة الأخطاء — كتل catch وفروع حراسة null يُغفلها المطورون عادةً

فئة بمسار خطأ غير مغطى عمداً

class PaymentService {
  final ApiClient _client;
  PaymentService(this._client);

  // هذه الطريقة لها مساران: النجاح والاستثناء المُلقى.
  // إذا اختبرت الاختبارات المسار السعيد فقط، يبقى كتلة catch حمراء.
  Future<Receipt> charge(double amount) async {
    try {
      final response = await _client.post('/charge', {'amount': amount});
      return Receipt.fromJson(response.data);
    } on ApiException catch (e) {
      // هذا الفرع أحمر حتى تكتب اختباراً يُحاكي
      // ApiException من عميل mock.
      throw PaymentFailedException(e.message);
    }
  }
}

// لتغطية مسار الخطأ، أضف اختباراً كهذا:
void main() {
  test('charge throws PaymentFailedException on ApiException', () async {
    final mockClient = MockApiClient();
    when(() => mockClient.post(any(), any()))
        .thenThrow(ApiException('Network error'));

    final service = PaymentService(mockClient);
    expect(
      () => service.charge(9.99),
      throwsA(isA<PaymentFailedException>()),
    );
  });
}

سير عمل عملي لتحسين التغطية

تحسين التغطية يكون أكثر فاعلية عندما يكون تدريجياً وهادفاً، لا مجرد مطاردة أرقام:

  • شغّل flutter test --coverage وولّد تقرير HTML كجزء من كل فرع ميزة.
  • ضع قاعدة الرقاط: لا يجوز أبداً أن تنخفض التغطية من طلب سحب إلى آخر، حتى لو ظل الحد المطلق منخفضاً في البداية.
  • أعطِ الأولوية للكود الحساس تجارياً (منطق الدفع، المصادقة، تسلسل البيانات) على حساب الحاصلات البسيطة والغراء الواجهي.
  • استخدم // coverage:ignore-line و// coverage:ignore-start … // coverage:ignore-end لاستبعاد الأسطر التي يتعذر اختبارها حقاً (قنوات المنصة، الكود المولّد المحتفظ به داخل المصدر) من التقرير.
تحذير: مطاردة نسبة تغطية 100% هي فخ. الاختبارات المكتوبة فقط لزيادة رقم ما كثيراً ما تختبر تفاصيل التنفيذ بدلاً من السلوك، مما يجعل إعادة الهيكلة مؤلمة. اهدف إلى تغطية ذات معنى لفروع المنطق، وليس عدد الأسطر النحوية.

ملخص

تشغيل flutter test --coverage ينتج تقرير LCOV. يحوّل genhtml هذا التقرير إلى تصور HTML تظهر فيه الأسطر غير المغطاة باللون الأحمر. تصفية الملفات المولّدة تمنع الأرقام المتضخمة. تفرض نصوص حد CI حداً أدنى للتغطية حتى يتم اكتشاف الانحدارات قبل الوصول إلى الإنتاج. أخيراً، ركّز جهد كتابة الاختبارات على المنطق الحساس تجارياً ومسارات معالجة الأخطاء والمسارات الشرطية المعقدة بدلاً من الزيادة العمياء لعدد الأسطر.