مطابقات اختبار الودجت ومحاكاة تفاعل المستخدم
مطابقات اختبار الودجت ومحاكاة تفاعل المستخدم
كتابة اختبارات ودجت ذات معنى تتطلب مهارتين متكاملتين: التحقق مما هو معروض على الشاشة باستخدام أدوات المطابقة المدمجة في Flutter، ومحاكاة طريقة تفاعل المستخدمين مع واجهتك باستخدام واجهة برمجة تطبيقات WidgetTester. معاً، تتيح لك هاتان التقنيتان كتابة اختبارات تعكس سلوك المستخدم الحقيقي دون الحاجة إلى تشغيل جهاز فعلي.
أدوات البحث والمطابقة الأساسية
كل اختبار ودجت يعتمد على أدوات البحث (Finders) لتحديد موقع الودجات وعلى أدوات المطابقة (Matchers) للتحقق من عددها. أهم ثلاث أدوات مطابقة هي:
findsOneWidget— تؤكد وجود ودجت واحد مطابق بالضبط في الشجرة. استخدمها للعناصر الفريدة مثل زر الإرسال أو العنوان الرئيسي.findsNothing— تؤكد عدم وجود أي ودجت مطابق. مثالية للتحقق من إخفاء رسالة خطأ أو مؤشر تحميل.findsNWidgets(n)— تؤكد وجود عدد n من الودجات المطابقة بالضبط. استخدمها عندما يجب أن تحتوي قائمة أو عنصر متكرر على عدد محدد.
تُمرَّر هذه أدوات المطابقة إلى دالة expect() في Flutter جنباً إلى جنب مع Finder الذي تنتجه أدوات مثل find.text() أو find.byType() أو find.byKey().
أمثلة على أدوات المطابقة الأساسية
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('counter page assertions', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterPage()),
);
// يجب وجود ودجت واحد يعرض '0'
expect(find.text('0'), findsOneWidget);
// لا ينبغي ظهور تسمية الخطأ عند التحميل الأول
expect(find.text('Error'), findsNothing);
// يجب أن تظهر ثلاثة عناصر في قائمة السجل
expect(find.byType(ListTile), findsNWidgets(3));
});
}
محاكاة النقرات باستخدام tester.tap()
تُرسل tester.tap(finder) إيماءة الضغط للأسفل ثم الأعلى على أول ودجت يطابقه المُحدِّد. بعد استدعاء tap()، يجب استدعاء await tester.pump() (أو pumpAndSettle()) لتفريغ قائمة انتظار الإطارات والسماح لـ Flutter بإعادة بناء شجرة الودجات قبل إجراء التحقق.
pump() تقدم الساعة بمقدار إطار واحد. أما pumpAndSettle() فتضخ الإطارات بشكل متكرر حتى لا تتبقى أي إطارات مجدولة — وهي مفيدة بعد الرسوم المتحركة أو العمليات غير المتزامنة. نسيان الضخ بعد النقرة هو أحد أكثر مصادر الاختبارات الخاطئة شيوعاً.محاكاة النقر على زر
testWidgets('tapping increment button increases counter', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterPage()),
);
// العداد يبدأ من 0
expect(find.text('0'), findsOneWidget);
// النقر على زر الإجراء العائم
await tester.tap(find.byType(FloatingActionButton));
// السماح لـ Flutter بإعادة البناء
await tester.pump();
// يجب أن يعرض العداد الآن 1
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});
إدخال النص باستخدام tester.enterText()
تُركِّز tester.enterText(finder, text) على حقل النص المطابق وتستبدل محتواه بالنص المحدد. هذا يحاكي الإدخال من لوحة المفاتيح دون الحاجة إلى لوحة مفاتيح فعلية. بعد إدخال النص، استدع await tester.pump() لمعالجة التغيير وتشغيل أي ردود نداء onChanged.
TextEditingController، تُحدَّث قيمة المتحكم بشكل متزامن — لكن أي استدعاءات setState() تعتمد عليه تحتاج إلى pump() للتفريغ. احرص دائماً على الضخ بعد enterText() قبل التحقق من تغييرات واجهة المستخدم.محاكاة إدخال النص والتحقق من صحة النموذج
testWidgets('login form shows error for empty email', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: LoginPage()),
);
// إدخال كلمة مرور صحيحة مع ترك حقل البريد الإلكتروني فارغاً
await tester.enterText(
find.byKey(const Key('password_field')),
'secret123',
);
// النقر على زر تسجيل الدخول لتشغيل التحقق
await tester.tap(find.byKey(const Key('login_button')));
await tester.pump();
// يجب ظهور نص خطأ لحقل البريد الإلكتروني
expect(find.text('Please enter your email'), findsOneWidget);
// إدخال بريد إلكتروني صحيح وإعادة الإرسال
await tester.enterText(
find.byKey(const Key('email_field')),
'user@example.com',
);
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle();
// يجب أن يختفي الخطأ
expect(find.text('Please enter your email'), findsNothing);
});
محاكاة التمرير باستخدام tester.drag()
تحاكي tester.drag(finder, offset) إيماءة السحب على الودجت المطابق بمقدار Offset المحدد. القيمة السالبة لـ dy تُمرر للأسفل (يتحرك المحتوى للأعلى)، بينما القيمة الموجبة تُمرر للأعلى. استخدم pumpAndSettle() بعد السحب للسماح لفيزياء التمرير بالاستقرار قبل التحقق من المحتوى الجديد الظاهر.
tester.drag() إيماءة سحب واحدة فورية. إذا كان عرض التمرير يستخدم رسوم متحركة للفيزياء (مثل BouncingScrollPhysics)، تستمر الرسوم المتحركة بعد انتهاء السحب. استخدم دائماً pumpAndSettle() بدلاً من pump() مفردة بعد التمرير لتجنب التحقق في حالة منتصف الرسوم المتحركة.التمرير للكشف عن ودجت
testWidgets('scrolling down reveals footer text', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: LongListPage()),
);
// التذييل خارج الشاشة في البداية
expect(find.text('End of list'), findsNothing);
// سحب القائمة للأعلى بمقدار 500 بكسل منطقي للتمرير للأسفل
await tester.drag(
find.byType(ListView),
const Offset(0, -500),
);
await tester.pumpAndSettle();
// يجب أن يكون التذييل مرئياً الآن
expect(find.text('End of list'), findsOneWidget);
});
دمج التفاعلات في اختبار واحد
تجمع تدفقات المستخدم الحقيقية إيماءات متعددة. يمكنك تسلسل استدعاءات tap() وenterText() وdrag() ضمن اختبار واحد لمحاكاة سير عمل كامل. احرص على متابعة كل تفاعل باستدعاء pump() أو pumpAndSettle() مناسب لما يُطلقه ذلك التفاعل.
الملخص
تمنحك مجموعة أدوات اختبار الودجت في Flutter تحكماً دقيقاً في كل من التحقق والتفاعل:
- استخدم
findsOneWidgetوfindsNothingوfindsNWidgets(n)للتحقق من وجود الودجات. - استخدم
tester.tap()لتشغيل ضغطات الأزرار وردود نداء الإيماءات. - استخدم
tester.enterText()لملء حقول النص برمجياً. - استخدم
tester.drag()لمحاكاة إيماءات التمرير. - استدع دائماً
pump()أوpumpAndSettle()بعد كل تفاعل قبل التحقق من النتيجة.