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

اختبار الودجت ذات الحالة والتنقل

15 دقيقة الدرس 8 من 12

اختبار الودجت ذات الحالة والتنقل

عندما تحتفظ الودجات بحالة داخلية أو تدفع مسارات إلى مكدس التنقل، تحتاج إلى تقنيات اختبار متخصصة تتجاوز استدعاءات find وexpect البسيطة. يتناول هذا الدرس أداتين أساسيتين: pumpAndSettle() للانتظار على مستوى الإطارات للرسوم المتحركة وإعادة بناء الحالة، وMockNavigatorObserver للتحقق من أن شيفرتك تستدعي إجراءات التنقل الصحيحة دون تشغيل مسار حقيقي.

لماذا تحتاج الودجات ذات الحالة عناية إضافية

يبني StatefulWidget نفسه من حالة قابلة للتغيير. بعد أن تتفاعل معه في اختبار (بالنقر على زر أو إرسال نموذج)، يحتاج الإطار إلى إطار أو أكثر لاستدعاء setState وجدولة إعادة بناء ورسم الشجرة الجديدة. إذا أجريت التحقق فور تشغيل التفاعل، قد لا تكون شجرة الودجات قد استقرت بعد وسيرى اختبارك واجهة مستخدم قديمة.

  • pump() يقدم الساعة بإطار واحد فقط — مفيد عندما تعرف بالضبط عدد الإطارات التي تستغرقها الرسوم المتحركة.
  • pumpAndSettle() يستمر في ضخ الإطارات حتى لا يبقى عمل معلق — الخيار الصحيح بعد تفاعلات المستخدم التي تطلق setState أو رسومات متحركة قصيرة.
  • تخطي أي من الاستدعاءين هو أكثر أسباب فشل الاختبارات السلبية الكاذبة شيوعاً.
ملاحظة: يمتلك pumpAndSettle() مهلة افتراضية مقدارها 100 ميلي ثانية وفترة إطار 16 مللي ثانية (60 إطاراً في الثانية). إذا كانت رسومك المتحركة تعمل لفترة أطول من 100 مللي ثانية، مرر مدة مخصصة: pumpAndSettle(const Duration(seconds: 2)).

مثال 1 — اختبار عداد بالتبديل

أدناه ودجت عداد ذو حالة واختبار ودجت كامل يتحقق من إعادة بناء واجهة المستخدم بشكل صحيح بعد النقر على زر الزيادة.

ودجت العداد + الاختبار

// counter_widget.dart
import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('Count: $_count', key: const Key('counter_text')),
        ElevatedButton(
          key: const Key('increment_btn'),
          onPressed: () => setState(() => _count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

// counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_widget.dart';

void main() {
  testWidgets('increments counter and rebuilds UI', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: Scaffold(body: CounterWidget())),
    );

    // الحالة الأولية
    expect(find.text('Count: 0'), findsOneWidget);

    // محاكاة النقر
    await tester.tap(find.byKey(const Key('increment_btn')));

    // السماح لـ Flutter بمعالجة setState وإعادة البناء
    await tester.pumpAndSettle();

    // التحقق من ظهور الحالة الجديدة
    expect(find.text('Count: 1'), findsOneWidget);
    expect(find.text('Count: 0'), findsNothing);
  });
}

اختبار إعادة بناء الودجت بعد تفاعلات متعددة

يمكنك تسلسل عدة تفاعلات داخل اختبار واحد للتحقق من تغييرات الحالة التراكمية. يجب أن يتبع كل تفاعل استدعاء pumpAndSettle() حتى تعكس شجرة الودجات أحدث حالة قبل التحقق التالي.

نصيحة: عيّن قيم Key للودجات التي تحتاج إلى إيجادها في الاختبارات. يجعل هذا الباحثين مقاومين لتغييرات النصوص وإعادة تنظيم شجرة الودجات.

اختبار التنقل باستخدام MockNavigatorObserver

عندما يستدعي زر Navigator.of(context).push(...)، لا تريد عادةً تصيير شاشة الوجهة في اختبار وحدة — سيسحب ذلك تبعيات غير ذات صلة. بدلاً من ذلك، تحقن نموذجاً مزيفاً من NavigatorObserver في MaterialApp وتتحقق من أن المراقب تلقى استدعاء didPush باسم المسار الصحيح.

يتطلب النمط ثلاث خطوات:

  • إنشاء فئة نموذج مزيف تمتد من NavigatorObserver وتتجاوز didPush.
  • تمرير نسخة النموذج المزيف إلى MaterialApp(navigatorObservers: [mockObserver]).
  • بعد النقر على محفز التنقل، استدعاء pumpAndSettle() ثم التحقق من استدعاء النموذج المزيف.

مثال 2 — التحقق من دفع مسار

يتحقق هذا الاختبار من أن الضغط على زر "الانتقال إلى التفاصيل" يدفع مساراً مسمى دون تصيير شاشة التفاصيل فعلياً.

اختبار نموذج NavigatorObserver المزيف

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

// 1. تعريف النموذج المزيف
class MockNavigatorObserver extends Mock implements NavigatorObserver {}

// الودجت الخاضع للاختبار
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ElevatedButton(
          key: const Key('go_details_btn'),
          onPressed: () => Navigator.of(context).pushNamed('/details'),
          child: const Text('Go to Details'),
        ),
      ),
    );
  }
}

void main() {
  testWidgets('pushes /details route when button is tapped', (tester) async {
    final mockObserver = MockNavigatorObserver();

    await tester.pumpWidget(
      MaterialApp(
        navigatorObservers: [mockObserver],
        routes: {
          '/': (_) => const HomeScreen(),
          '/details': (_) => const Scaffold(body: Text('Details')),
        },
        initialRoute: '/',
      ),
    );

    // النقر على زر التنقل
    await tester.tap(find.byKey(const Key('go_details_btn')));
    await tester.pumpAndSettle();

    // التحقق من إخطار المراقب بعملية الدفع
    verify(mockObserver.didPush(any, any)).called(greaterThanOrEqualTo(1));
  });
}
تحذير: يحسب verify(mockObserver.didPush(any, any)) جميع عمليات الدفع، بما في ذلك دفع المسار الأولي عند بدء التطبيق. استخدم called(greaterThanOrEqualTo(1)) أو التقاط وسيطة المسار للتحقق من اسم مسار محدد بدلاً من أي دفع.

التقاط المسار المدفوع والتحقق منه بدقة

للتحقق من المسار الدقيق الذي تم دفعه، تجاوز didPush في فئة فرعية مخصصة من المراقب لالتقاط وسيطة المسار، ثم افحصها بعد التفاعل:

  • تجاوز didPush(Route route, Route? previousRoute) في فئة فرعية مخصصة من المراقب.
  • خزّن اسم المسار المدفوع في متغير محلي داخل التجاوز.
  • تحقق من أن هذا المتغير يساوي اسم المسار المتوقع بعد الضخ.
نصيحة: عند استخدام go_router بدلاً من Navigator المدمج، يُفضل اختبار المسارات عبر تأكيدات إعادة التوجيه والموقع الخاصة بـ GoRouter بدلاً من نماذج NavigatorObserver الخام، لأن go_router يُغلّف المتنقل الأساسي.

ملخص

تتطلب اختبارات الودجات ذات الحالة مزامنة مسخرة الاختبار مع جدولة إطارات Flutter. استدع pumpAndSettle() بعد كل تفاعل مستخدم يطلق تغيير حالة أو رسوماً متحركة. للتنقل، أدخل MockNavigatorObserver في شجرة الودجات للتحقق من دفع المسارات دون تصيير شاشات الوجهة الكاملة. معاً، تمنحك هاتان التقنيتان اختبارات سريعة وموثوقة تتحقق من سلوك كل من حالة واجهة المستخدم والتوجيه.