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

الاختبارات الذهبية لاكتشاف الانحدار المرئي

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

الاختبارات الذهبية لاكتشاف الانحدار المرئي

مع نمو تطبيقات Flutter في التعقيد، يصبح ضمان عدم تأثير التغييرات في واجهة المستخدم على المظهر البصري للودجات الموجودة أمراً بالغ الأهمية. تحل الاختبارات الذهبية (المعروفة أيضاً باختبارات اللقطات أو الصور) هذا الأمر عبر التقاط صورة مرجعية بدقة البكسل للودجت — تسمى الملف الذهبي — ثم المقارنة التلقائية لكل تشغيل اختباري لاحق بها. أي انحراف على مستوى البكسل يتسبب في فشل الاختبار، مما يوفر تغذية راجعة فورية وحتمية حول الانحدار البصري غير المقصود.

ملاحظة: تُكمل الاختبارات الذهبية اختبارات الوحدة والودجات، لكنها لا تحل محلها. استخدمها لحماية مكونات واجهة المستخدم المصقولة والمستقرة من التغييرات البصرية العرضية — وليس كاستراتيجية اختبار أساسية.

كيف تعمل الاختبارات الذهبية

الآلية مباشرة:

  • التشغيل الأول (توليد): تستدعي matchesGoldenFile('name.png') داخل expectLater. يُصيّر Flutter الودجت خارج الشاشة، يحفظ ملف PNG في مجلد test/، ويمر الاختبار.
  • التشغيلات اللاحقة (مقارنة): يُصيّر Flutter الودجت مجدداً ويُجري مقارنة بكسل بكسل مع ملف PNG المحفوظ. أي عدم تطابق يتسبب في فشل الاختبار مع صورة الفرق.
  • التحديثات المقصودة: عند تغيير واجهة المستخدم عن قصد، تُعيد توليد الملفات الذهبية بتشغيل الاختبارات مع الراية --update-goldens.

إعداد أول اختبار ذهبي

لا توجد حزم إضافية مطلوبة — دعم الملفات الذهبية مدمج في flutter_test. ضع ملفات الاختبار في مجلد test/ واستخدم testWidgets كالمعتاد.

مثال على اختبار ذهبي أساسي

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/profile_card.dart';

void main() {
  testWidgets('ProfileCard renders correctly - golden', (WidgetTester tester) async {
    // 1. تشغيل الودجت في بيئة متحكم بها
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData.light(),
        home: Scaffold(
          body: ProfileCard(
            name: 'Edrees Salih',
            role: 'Flutter Developer',
            avatarUrl: 'assets/test/avatar.png',
          ),
        ),
      ),
    );

    // 2. الانتظار حتى تستقر جميع الإطارات غير المتزامنة (صور، رسوم متحركة)
    await tester.pumpAndSettle();

    // 3. مقارنة تصيير الودجت مع الملف الذهبي
    await expectLater(
      find.byType(ProfileCard),
      matchesGoldenFile('goldens/profile_card.png'),
    );
  });
}

شغّل الاختبار للمرة الأولى بـ:

# توليد الملف الذهبي (للمرة الأولى فقط، أو بعد تغييرات واجهة المستخدم المقصودة)
flutter test --update-goldens test/widgets/profile_card_test.dart

# تشغيل المقارنة في كل تشغيل اختباري لاحق (CI / التطوير الاعتيادي)
flutter test test/widgets/profile_card_test.dart
نصيحة: احرص على حفظ ملفات PNG الذهبية في التحكم بالإصدار جنباً إلى جنب مع كود الاختبار. بهذه الطريقة يمكن للمراجعين فحص أي تغييرات في الملفات الذهبية بصرياً في طلبات السحب قبل دمجها.

التحكم في بيئة الاختبار لتحقيق الاتساق

الاختبارات الذهبية حساسة لبيئة التصيير. قد يُصيَّر نفس الودجت بشكل مختلف على أنظمة تشغيل مختلفة أو كثافات شاشة أو محركات خطوط. للحصول على نتائج حتمية عبر أجهزة المطورين وبيئة CI، ينبغي تثبيت حجم السطح وتصيير الخطوط.

اختبار ذهبي حتمي بحجم سطح ثابت

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/status_badge.dart';

void main() {
  testWidgets('StatusBadge active state - golden', (WidgetTester tester) async {
    // تثبيت السطح بحجم يمكن التنبؤ به لتجنب الاختلافات بين المنصات
    tester.view.physicalSize = const Size(400, 200);
    tester.view.devicePixelRatio = 1.0;

    addTearDown(() {
      tester.view.resetPhysicalSize();
      tester.view.resetDevicePixelRatio();
    });

    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: Center(
            child: StatusBadge(status: BadgeStatus.active),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();

    await expectLater(
      find.byType(StatusBadge),
      matchesGoldenFile('goldens/status_badge_active.png'),
    );
  });
}

تحديث الملفات الذهبية

عند إعادة تصميم مكوّن عن قصد — تغيير الألوان أو التباعد أو الطباعة أو التخطيط — يصبح الملف الذهبي الحالي قديماً. أعد توليده بالراية --update-goldens:

# تحديث جميع الملفات الذهبية في مجموعة الاختبار بالكامل
flutter test --update-goldens

# تحديث الملفات الذهبية لملف اختبار محدد فقط
flutter test --update-goldens test/widgets/profile_card_test.dart
تحذير: لا تشغّل --update-goldens بشكل أعمى في CI دون مراجعة بشرية. يجب استخدام هذه الراية محلياً فقط بعد تغيير واجهة المستخدم عن قصد، ويجب مراجعة الفروقات الناتجة في ملفات PNG ضمن مراجعة الكود قبل الدمج. إعادة التوليد الأعمى تُفسد الغرض الكامل من الحماية من الانحدار البصري.

دمج الاختبارات الذهبية في CI

الاختبارات الذهبية أكثر قيمة عندما تعمل تلقائياً في كل طلب سحب. بعض أفضل ممارسات CI:

  • شغّل الاختبارات دائماً بدون --update-goldens في CI — الفشل إشارة مقصودة.
  • استخدم عامل تشغيل CI مبني على Linux واحفظ الملفات الذهبية من نفس المنصة لتجنب اختلافات تصيير الخطوط بين macOS وLinux.
  • فكر في حزمة flutter_goldens أو alchemist لسير عمل اختبار ذهبي أكثر تقدماً، بما في ذلك خطوط الأساس لكل منصة.
  • خزّن ملفات PNG الذهبية في مجلد فرعي مخصص test/goldens/ للحفاظ على ترتيب شجرة الاختبار.
نصيحة: تُوسّع حزمة alchemist اختبارات Flutter الذهبية بملفات ذهبية خاصة بكل منصة، ومخرجات فشل مقروءة، وتمييز أفضل بين CI والتشغيل المحلي — تستحق التقييم للمشاريع الكبيرة.

الخلاصة

الاختبارات الذهبية شبكة أمان قوية ومنخفضة التكلفة للانحدار البصري. سير العمل الأساسي هو: تشغيل الودجت في بيئة متحكم بها، تثبيت جميع الإطارات بـ pumpAndSettle()، ثم التأكيد بـ matchesGoldenFile(). ولّد الخطوط الأساسية مرة واحدة بـ --update-goldens، احفظ ملفات PNG في التحكم بالإصدار، ودع CI يقارن كل تصيير مستقبلي بها. ثبّت حجم السطح ونسبة بكسل الجهاز لتحقيق الحتمية عبر المنصات، وراجع دائماً الفروقات الذهبية جنباً إلى جنب مع تغييرات الكود.