كيف تختبر Futures وStreams بثقة
اختبار الكود غير المتزامن في Flutter ، تخيل أنك تبني تطبيق Flutter يتصل بخادم خارجي للحصول على بيانات المستخدم. تكتب الكود، كل شيء يعمل… ثم فجأة أثناء الاختبار تظهر نتائج غريبة:
القيمة تصل متأخرة، الاختبار ينتهي قبل التنفيذ، أو النتائج تختلف من مرة لأخرى!
السبب؟
لأنك تتعامل مع كود غير متزامن (Asynchronous Code) — أي كود لا يُنفّذ فورًا، بل ينتظر عمليات أخرى (مثل تحميل البيانات أو التعامل مع API).
ولكي تختبره بطريقة صحيحة، عليك أن تفهم كيف يفكر Flutter عندما ينتظر الـ Futures والـ Streams.
ما هو الكود غير المتزامن في Dart وFlutter؟
في Dart، أي عملية تستغرق وقتًا — مثل قراءة ملف، أو انتظار API، أو مؤقت زمني — تُنفَّذ بشكل غير متزامن.
أي أنها لا توقف البرنامج، بل تعمل في الخلفية.
أشهر ثلاث أدوات لذلك هي:
- Future: تمثل قيمة ستصل في المستقبل (مثل نتيجة من API).
- Stream: سلسلة من القيم التي تصل تدريجيًا بمرور الوقت.
- async / await: أدوات تجعل كتابة الكود غير المتزامن أسهل وأكثر وضوحًا.
✨ بعبارة بسيطة:
Future = “انتظر القيمة”،
Stream = “تابع القيم”،
async/await = “اجعل الانتظار يبدو طبيعيًا في الكود”.
التحدي في الاختبار
عندما تختبر دالة غير متزامنة، لا يمكنك فقط استدعاؤها والتحقق من النتيجة فورًا.
فقد لا تكون النتيجة قد وصلت بعد!
لذلك تحتاج إلى انتظار التنفيذ الكامل قبل التحقق من النتيجة.
لحسن الحظ، Flutter Test Framework يدعم async وawait بشكل طبيعي.
🧩 مثال بسيط: اختبار Future
Future<String> fetchUserName() async {
await Future.delayed(Duration(seconds: 1));
return 'Ahmed';
}
والآن نختبرها:
import 'package:flutter_test/flutter_test.dart';
void main() {
test('يجب أن يعيد اسم المستخدم بعد التأخير', () async {
final result = await fetchUserName();
expect(result, equals('Ahmed'));
});
}
لاحظ هنا أننا أضفنا الكلمة async إلى جسم الاختبار، واستخدمنا await قبل استدعاء الدالة.
بهذا الشكل ينتظر Flutter حتى تكتمل العملية قبل التحقق من النتيجة. ✅
اختبار Stream
الـ Stream أكثر تعقيدًا لأنه لا يُعيد قيمة واحدة فقط، بل عدة قيم متتالية.
لنأخذ هذا المثال:
Stream<int> counterStream() async* {
for (var i = 1; i <= 3; i++) {
await Future.delayed(Duration(milliseconds: 500));
yield i;
}
}
الآن نريد التأكد أن الـ Stream يُرسل القيم بالترتيب الصحيح.
void main() {
test('يجب أن يُرسل القيم من 1 إلى 3', () async {
final values = await counterStream().toList();
expect(values, equals([1, 2, 3]));
});
}
هنا استخدمنا .toList() لتحويل الـ Stream إلى قائمة، وانتظرنا كل القيم قبل المقارنة.
بهذا نضمن أن الاختبار ينتظر حتى نهاية السلسلة.
اختبار الكود غير المتزامن داخل Widget
عند اختبار Widgets تحتوي على عمليات غير متزامنة (مثل تحميل بيانات أو مؤقتات)، يمكننا استخدام pump وpumpAndSettle للتحكم في زمن التنفيذ داخل بيئة الاختبار.
مثلاً:
testWidgets('عرض مؤشر تحميل أثناء انتظار البيانات', (tester) async {
await tester.pumpWidget(MyAsyncWidget());
// في البداية، يجب أن يظهر مؤشر التحميل
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// انتظر اكتمال الـ Future داخل الواجهة
await tester.pumpAndSettle();
// بعد انتهاء التحميل، يجب أن تظهر النتيجة
expect(find.text('Data Loaded!'), findsOneWidget);
});
هنا يقوم pumpAndSettle() بمحاكاة مرور الزمن حتى تكتمل جميع العمليات غير المتزامنة داخل واجهة المستخدم.
اختبار الاستثناءات في Futures
يمكنك اختبار الأخطاء (Exceptions) بنفس سهولة اختبار القيم الصحيحة:
Future<void> failFunction() async {
throw Exception('حدث خطأ!');
}
test('يجب أن يرمي استثناء عند الفشل', () async {
expect(failFunction(), throwsException);
});
أو بشكل أكثر تحديدًا:
expect(failFunction(), throwsA(isA<Exception>()));
أدوات مفيدة لاختبار الكود غير المتزامن
| الأداة | الاستخدام |
|---|---|
fake_async | لمحاكاة مرور الوقت في Futures وTimers دون تأخير فعلي |
pumpAndSettle() | لإدارة الزمن في اختبارات Widgets |
expectLater() | لاختبار قيم مستقبلية تصل لاحقًا |
throwsA() | لاختبار الاستثناءات المستقبلية |
مثال متقدم باستخدام fake_async
أحيانًا لا تريد أن تنتظر ثانية أو اثنتين في كل اختبار — بل تريد محاكاة مرور الزمن بسرعة.
هنا يأتي دور fake_async:
import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
Future<int> delayedValue() async {
await Future.delayed(Duration(seconds: 3));
return 42;
}
void main() {
test('اختبار بدون انتظار فعلي', () {
fakeAsync((async) {
var result;
delayedValue().then((value) => result = value);
async.elapse(Duration(seconds: 3)); // نحاكي مرور الوقت
expect(result, 42);
});
});
}
بهذه الطريقة، تنفّذ اختباراتك فوريًا دون أي تأخير حقيقي. ⚡
التحديات الشائعة ونصائح لتفاديها
- لا تنسَ كتابة
asyncفي دوال الاختبار عند استخدامawait. - استخدم
pumpAndSettleبدلًا منpumpعندما تتوقع عدة عمليات متتابعة. - اختبر السيناريوهات السلبية (مثل أخطاء الشبكة أو التأخير المفرط).
- لا تعتمد على قيم عشوائية أو مؤقتات حقيقية في الاختبار.
الخلاصة
اختبار الكود غير المتزامن هو خطوة أساسية لكل مطور Flutter جاد في ضمان استقرار تطبيقه.
فهو يكشف الأخطاء الخفية التي تظهر فقط عند انتظار العمليات أو التعامل مع البيانات الحية.
ابدأ بتطبيق هذه التقنيات اليوم، وسترى كيف تتحول اختباراتك من مجرد تحقق بسيط إلى منظومة ذكية تعرف متى تنتظر ومتى تحكم بالنجاح.
✨ تذكّر: السر في اختبار الكود غير المتزامن هو “أن تجعل الزمن تحت سيطرتك”.
