اختبار الكود غير المتزامن والتدفقات
اختبار الكود غير المتزامن والتدفقات
تطبيقات Flutter في العالم الحقيقي مليئة بالعمليات غير المتزامنة: جلب البيانات من واجهة برمجية، والقراءة من التخزين المحلي، ومصادقة المستخدمين، والاستماع إلى تدفقات البيانات الفورية. كتابة اختبارات تتحقق بشكل صحيح من هذا السلوك غير المتزامن يتطلب أدوات مخصصة يوفرها حزمة flutter_test. في هذا الدرس ستتعلم كيف تختبر الكود المبني على Future باستخدام async/await ومطابق completion()، وكيف تختبر الكود المبني على Stream باستخدام expectLater() ومطابق emitsInOrder().
لماذا اختبار الكود غير المتزامن مختلف؟
الاختبارات المتزامنة تنهي تأكيداتها قبل أن تُعيد دالة الاختبار. الاختبارات غير المتزامنة يجب أن تنتظر حتى تنحل الـ futures أو تُصدر التدفقات قيمها قبل أن يمكن إجراء أي تأكيد. إذا نسيت await على تأكيد غير متزامن، فسيجتاز الاختبار ببساطة — لأن التأكيدات لن تُنفَّذ أبداً — مما يمنحك إحساساً زائفاً بالأمان.
- دائماً ضع علامة
asyncعلى دوال الاختبار عند الحاجة إلىawaitبداخلها. - استخدم
expectLater()(تُعيدFuture) بدلاً منexpect()للمطابقات التي تنحل بشكل غير متزامن. - استدع
awaitعلى نتيجةexpectLater()حتى ينتظر الاختبار اكتمال التأكيد.
expectLater() مطابق لـ expect() إلا أنه يُعيد Future<void>. دائماً استخدم await معه؛ وإلا قد ينتهي الاختبار قبل انتهاء تقييم المطابق.اختبار الـ Futures باستخدام async/await و completion()
أبسط طريقة لاختبار Future هي await نتيجته ثم تشغيل تأكيد متزامن عادي:
انتظار Future مباشرة
import 'package:flutter_test/flutter_test.dart';
Future<String> fetchGreeting(String name) async {
await Future.delayed(const Duration(milliseconds: 50));
return 'Hello, $name!';
}
void main() {
test('fetchGreeting returns correct message', () async {
final result = await fetchGreeting('Edrees');
expect(result, equals('Hello, Edrees!'));
});
}
في الحالات التي تريد فيها التأكيد فقط على أن Future يكتمل بنجاح (دون الاهتمام بالقيمة الدقيقة)، استخدم مطابق completion() مع expectLater():
استخدام completion() و expectLater()
import 'package:flutter_test/flutter_test.dart';
Future<int> computeSquare(int n) async {
await Future.delayed(const Duration(milliseconds: 10));
return n * n;
}
void main() {
test('computeSquare(5) completes with 25', () async {
// completion() يلف أي مطابق ويتأكد من أن الـ future ينحل له
await expectLater(
computeSquare(5),
completion(equals(25)),
);
});
test('computeSquare completes without throwing', () async {
// completes مطابق مريح — ينجح إذا تحل الـ future
await expectLater(computeSquare(3), completes);
});
test('an invalid operation throws', () async {
// throwsA يتحقق من أن الـ future يرفض بنوع خطأ معين
await expectLater(
Future<void>.error(ArgumentError('bad input')),
throwsA(isA<ArgumentError>()),
);
});
}
completion(matcher) عندما تهمك القيمة المُحلَّة للـ future. استخدم completes عندما تهمك فقط عدم الرمي. استخدم throwsA(isA<ExceptionType>()) للتأكيد على مسارات الأخطاء المتوقعة.اختبار التدفقات (Streams) باستخدام expectLater() و emitsInOrder()
التدفقات تُصدر صفراً أو أكثر من القيم عبر الزمن قبل أن تنتهي أو تُصدر خطأ بشكل اختياري. مطابق emitsInOrder() يتيح لك الإعلان عن التسلسل الكامل للأحداث المتوقعة والتحقق منها بالترتيب:
اختبار تسلسل Stream
import 'package:flutter_test/flutter_test.dart';
Stream<int> countDown(int from) async* {
for (var i = from; i >= 1; i--) {
await Future.delayed(const Duration(milliseconds: 10));
yield i;
}
}
void main() {
test('countDown(3) emits 3, 2, 1 in order then closes', () async {
await expectLater(
countDown(3),
emitsInOrder([3, 2, 1, emitsDone]),
);
});
test('stream emits an error', () async {
final errorStream = Stream<int>.error(StateError('broken'));
await expectLater(
errorStream,
emitsError(isA<StateError>()),
);
});
}
مطابقات Stream الرئيسية
emitsInOrder([...])— يتأكد من أن كل قيمة تُصدر بالترتيب المذكور؛ لف القيم الفردية بـemits()أو استخدم القيم الخام (تُلف تلقائياً).emits(value)— يتأكد من أن التدفق يُصدر قيمة معينة واحدة بعد ذلك.emitsError(matcher)— يتأكد من أن التدفق يُصدر خطأ يطابق المطابق المعطى.emitsDone— يتأكد من أن التدفق يُغلق (حدث done) بدون قيم إضافية.neverEmits(matcher)— يتأكد من أن التدفق لا يُصدر أبداً قيمة تطابق المطابق قبل الإغلاق.mayEmit(matcher)— يطابق قيمة بشكل اختياري؛ ينجح سواء صدرت القيمة أم لا.
emitsDone في نهاية قائمة emitsInOrder إذا أردت التأكيد على أن التدفق يُغلق بعد القيم المتوقعة. إغفاله يعني عدم اكتشاف الأحداث الإضافية غير المتوقعة.اختبار مستودع يعرض تدفقاً
في الواقع العملي ستختبر في الغالب صنفاً للخدمة أو المستودع بدلاً من تدفق خام. النمط هو نفسه — اصطنع التبعيات، استدع الطريقة، وأكِّد على التدفق المُعاد:
نمط اختبار تدفق المستودع
import 'package:flutter_test/flutter_test.dart';
// خدمة عداد مبنية على تدفق بسيطة
class CounterService {
final _controller = StreamController<int>.broadcast();
Stream<int> get counterStream => _controller.stream;
void increment(int current) => _controller.add(current + 1);
void dispose() => _controller.close();
}
void main() {
late CounterService service;
setUp(() => service = CounterService());
tearDown(() => service.dispose());
test('counterStream emits incremented values', () async {
final future = expectLater(
service.counterStream,
emitsInOrder([1, 2, 3]),
);
service.increment(0);
service.increment(1);
service.increment(2);
await future;
});
}
tearDown لإغلاق وحدات تحكم التدفق. تركها مفتوحة يسبب تسريباً في الموارد ويمكن أن يُفشل الاختبارات اللاحقة بأحداث غير متوقعة.الملخص
اختبار كود Dart غير المتزامن يتطلب فهم مجموعة صغيرة لكن مهمة من القواعد والمطابقات:
- ضع علامة
asyncعلى دوال الاختبار واستخدمawaitمع جميع التأكيدات غير المتزامنة لمنع الاجتياز الزائف. - استخدم
awaitمع التأكيد المباشر للـ futures البسيطة؛ استخدمcompletion()معexpectLater()للتأكيدات الأكثر تعبيراً. - استخدم
throwsA()للتحقق من أن الـ futures ترفض بنوع الخطأ الصحيح. - استخدم
emitsInOrder()معexpectLater()للتحقق من التسلسل الكامل لأحداث التدفق بما في ذلك الأخطاء وحدث الإنهاء. - نظِّف وحدات تحكم التدفق في
tearDownللحفاظ على موثوقية مجموعة الاختبارات.