اختبار مزودي Riverpod والمُخبِرين
اختبار مزودي Riverpod والمُخبِرين
كتابة الاختبارات لكود Riverpod هي من أقوى مزايا هذا الإطار. نظرًا لأن المزودين (providers) عبارة عن كائنات Dart نقية والاعتماديات تُحقن عبر الحاوية، يمكنك استبدال أي مزود بنسخة وهمية أو بديلة، وعزل وحدات المنطق بوضوح، والتحقق من انتقالات حالة AsyncValue دون الحاجة إلى تشغيل خادم حقيقي.
يغطي هذا الدرس اختبار وحدات Notifier وAsyncNotifier باستخدام ProviderContainer، والتأكيد على تسلسلات تحميل/بيانات/خطأ لـAsyncValue، وكتابة اختبارات الودجات التي تستبدل المزودين الحقيقيين بنسخ وهمية محكومة باستخدام استبدالات ProviderScope.
flutter_riverpod وriverpod إلى pubspec.yaml تحت dependencies وdev_dependencies حسب الحاجة. تتوجد فئة ProviderContainer في حزمة riverpod الأساسية ولا تتطلب Flutter، مما يجعلها مثالية لاختبارات Dart النقية.اختبار الوحدة باستخدام ProviderContainer
ProviderContainer هو الكائن المنخفض المستوى الذي يمتلك المزودين ويُقيّمهم. في الاختبارات تُنشئه يدويًا، وتمرر اختياريًا overrides لاستبدال الاعتماديات الحقيقية بنسخ وهمية، ثم تقرأ الحالة مباشرة دون أي شجرة ودجات.
إعداد الاختبار الأساسي باستخدام ProviderContainer
// counter_notifier.dart
import 'package:riverpod/riverpod.dart';
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
// counter_notifier_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';
void main() {
group('CounterNotifier', () {
late ProviderContainer container;
setUp(() {
// إنشاء حاوية جديدة قبل كل اختبار
container = ProviderContainer();
});
tearDown(() {
// تخلص دائمًا لمنع تسرب الذاكرة
container.dispose();
});
test('initial state is 0', () {
expect(container.read(counterProvider), 0);
});
test('increment increases state by 1', () {
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
test('decrement below zero is allowed', () {
container.read(counterProvider.notifier).decrement();
expect(container.read(counterProvider), -1);
});
test('reset returns state to 0', () {
container.read(counterProvider.notifier).increment();
container.read(counterProvider.notifier).increment();
container.read(counterProvider.notifier).reset();
expect(container.read(counterProvider), 0);
});
});
}
استبدال الاعتماديات في الاختبارات
القوة الحقيقية لـProviderContainer تكمن في حقن الاعتماديات عبر الاستبدالات. افترض أن المُخبِر يعتمد على مزود مستودع. في الاختبارات تستبدل المستودع الحقيقي بنسخة وهمية تُعيد بيانات يمكن التنبؤ بها — بدون طلبات HTTP أو قاعدة بيانات أو مؤقتات.
استبدال مزود المستودع
// user_repository.dart
abstract class UserRepository {
Future<String> fetchUsername(int id);
}
final userRepositoryProvider = Provider<UserRepository>((ref) {
throw UnimplementedError('Provide a real implementation');
});
// user_notifier.dart
class UserNotifier extends AsyncNotifier<String> {
@override
Future<String> build() async {
final repo = ref.watch(userRepositoryProvider);
return repo.fetchUsername(1);
}
}
final userProvider = AsyncNotifierProvider<UserNotifier, String>(
UserNotifier.new,
);
// user_notifier_test.dart
class FakeUserRepository implements UserRepository {
final String name;
FakeUserRepository(this.name);
@override
Future<String> fetchUsername(int id) async => name;
}
void main() {
test('userProvider loads username from repository', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(
FakeUserRepository('Alice'),
),
],
);
addTearDown(container.dispose);
// انتظر اكتمال البناء غير المتزامن
final result = await container.read(userProvider.future);
expect(result, 'Alice');
});
}
التحقق من انتقالات حالة AsyncValue
تمتلك AsyncValue<T> ثلاث حالات: AsyncLoading وAsyncData<T> وAsyncError. عند اختبار المُخبِرين غير المتزامنين كثيرًا ما تريد التأكيد على التسلسل الكامل — التحميل أولًا، ثم البيانات (أو الخطأ). استخدم ProviderSubscription لالتقاط كل انبعاث.
التأكيد على انتقالات حالة AsyncValue
test('shows loading then data', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(
FakeUserRepository('Bob'),
),
],
);
addTearDown(container.dispose);
final states = <AsyncValue<String>>[];
final sub = container.listen<AsyncValue<String>>(
userProvider,
(previous, next) => states.add(next),
fireImmediately: true,
);
// اسمح للمستقبل بالاكتمال
await container.read(userProvider.future);
sub.close();
// الانبعاث الأول هو AsyncLoading والأخير هو AsyncData
expect(states.first, isA<AsyncLoading<String>>());
expect(states.last, isA<AsyncData<String>>());
expect(states.last.value, 'Bob');
});
test('emits AsyncError when repository throws', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWith(
(ref) => throw Exception('network error'),
),
],
);
addTearDown(container.dispose);
final value = await container.read(userProvider.future).catchError((_) => '');
final state = container.read(userProvider);
expect(state, isA<AsyncError<String>>());
expect((state as AsyncError).error.toString(), contains('network error'));
});
اختبارات الودجات باستخدام استبدالات ProviderScope
لاختبارات الودجات، لف الودجت قيد الاختبار في ProviderScope ومرر نفس قائمة overrides. هذا يضمن أن كل قراءة مزود داخل شجرة الودجات تُعيد قيم النسخ الوهمية المحكومة بدلًا من التطبيقات الحية.
اختبار ودجت باستخدام استبدالات ProviderScope
// user_screen.dart (simplified)
class UserScreen extends ConsumerWidget {
const UserScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (name) => Text('Hello, $name'),
);
}
}
// user_screen_test.dart
testWidgets('shows username after load', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userRepositoryProvider.overrideWithValue(
FakeUserRepository('Carol'),
),
],
child: const MaterialApp(home: UserScreen()),
),
);
// الإطار الأول: يبدأ البناء غير المتزامن — يظهر مؤشر التحميل
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// حل جميع المستقبلات المعلقة وإعادة البناء
await tester.pumpAndSettle();
expect(find.text('Hello, Carol'), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
testWidgets('shows error message on failure', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userRepositoryProvider.overrideWith(
(ref) => throw Exception('timeout'),
),
],
child: const MaterialApp(home: UserScreen()),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('Error:'), findsOneWidget);
});
addTearDown(container.dispose) في اختبارات الوحدة واسمح لإطار اختبار الودجات بالتخلص من ProviderScope تلقائيًا. نسيان التخلص من الحاوية يُسرّب المستمعين ويمكن أن يتسبب في تداخل الاختبارات عند تشغيل المجموعة الكاملة.اختبار طرق المُخبِر التي تُطلق آثارًا جانبية غير متزامنة
عندما تستدعي طريقة مُخبِر طريقة مستودع غير متزامنة وتحدّث الحالة، استخدم container.listen مع fireImmediately: true لالتقاط كل انتقال، ثم استدع الطريقة وانتظر المستقبل الناتج المكشوف عبر المزود الفرعي .future.
container.read(provider) على AsyncNotifierProvider قبل أن يُستمع إلى المزود مرة واحدة على الأقل — طريقة البناء تعمل بشكل كسول. اصل إلى .future أو اشترك عبر container.listen لتشغيل البناء الأول.الخلاصة
هندسة Riverpod تجعل الاختبار سهلًا:
- استخدم
ProviderContainerمعoverridesلاختبار وحدات المُخبِرين في عزلة. - أكّد على حالات
AsyncValueمن تحميل وبيانات وخطأ بجمع الانبعاثات عبرcontainer.listen. - استخدم
ProviderScope(overrides: [...])لحقن نسخ وهمية في اختبارات الودجات دون تغيير كود الإنتاج. - تخلص دائمًا من الحاويات (اختبارات الوحدة) واستخدم
addTearDownللحفاظ على الاختبارات منغلقة على نفسها. - تحقق من كل من المسار السعيد (تحميل → بيانات) ومسار الخطأ (تحميل → خطأ) لضمان واجهات مستخدم قوية.