الاختبار: اختبارات الوحدة والودجت والتكامل
الاختبار: اختبارات الوحدة والودجت والتكامل
لا يكتمل تطبيق Flutter الاحترافي بدون مجموعة اختبارات متينة. يوفر Flutter ثلاث طبقات اختبار متكاملة: اختبارات الوحدة لمنطق الأعمال، اختبارات الودجت لمكونات واجهة المستخدم، واختبارات التكامل للتدفقات الكاملة من النهاية إلى النهاية. إتقان الثلاثة معاً يتيح لك الشحن بثقة وإعادة الهيكلة دون قلق.
اختبارات الوحدة — اختبار حالات الاستخدام والمستودعات
تتحقق اختبارات الوحدة من أصغر القطع المنطقية المعزولة — حالات الاستخدام والمستودعات والمحولات والدوال المساعدة — دون لمس إطار Flutter أو أي خدمة خارجية حقيقية. الحزمة القياسية هي flutter_test (موجودة بالفعل في dev_dependencies)؛ استخدم mockito أو mocktail لاستبدال الاعتماديات الحقيقية بأخرى وهمية.
مشروع Capstone النموذجي له شكل البنية النظيفة: واجهة المستودع ← تنفيذ المستودع ← حالة الاستخدام. اختبر حالة الاستخدام مقابل مستودع وهمي حتى لا يلمس اختبارك قاعدة بيانات أو شبكة.
اختبار وحدة: AuthUseCase مع مستودع وهمي
// test/domain/use_cases/sign_in_use_case_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/domain/repositories/auth_repository.dart';
import 'package:my_app/domain/use_cases/sign_in_use_case.dart';
import 'package:my_app/domain/entities/user.dart';
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
late MockAuthRepository mockRepo;
late SignInUseCase useCase;
setUp(() {
mockRepo = MockAuthRepository();
useCase = SignInUseCase(mockRepo);
});
group('SignInUseCase', () {
const tEmail = 'user@example.com';
const tPassword = 'secret123';
final tUser = User(id: '1', email: tEmail, name: 'Test User');
test('returns User on successful sign-in', () async {
when(() => mockRepo.signIn(email: tEmail, password: tPassword))
.thenAnswer((_) async => tUser);
final result = await useCase(email: tEmail, password: tPassword);
expect(result, tUser);
verify(() => mockRepo.signIn(email: tEmail, password: tPassword))
.called(1);
});
test('rethrows AuthException on failure', () async {
when(() => mockRepo.signIn(email: tEmail, password: tPassword))
.thenThrow(AuthException('Invalid credentials'));
expect(
() => useCase(email: tEmail, password: tPassword),
throwsA(isA<AuthException>()),
);
});
});
}
test() مركّزاً على سلوك واحد. يعيد كتلة setUp() إنشاء مكوكات جديدة قبل كل اختبار، مما يمنع تسرب الحالة بين الحالات.اختبارات الودجت — التحقق من مكونات واجهة المستخدم الرئيسية
تُصيِّر اختبارات الودجت ودجتاً في بيئة Flutter بدون رأس (لا يلزم جهاز حقيقي) وتتيح لك التفاعل معه برمجياً. استخدم WidgetTester لضخ الودجات والنقر على الأزرار وإدخال النص والتأكد من شجرة الودجات. تعمل بسرعة أكبر بكثير من اختبارات التكامل وتكتشف التراجعات في المكونات الفردية.
اختبار ودجت: يعرض LoginForm ويرسل البيانات
// test/presentation/widgets/login_form_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:my_app/presentation/widgets/login_form.dart';
import 'package:my_app/presentation/providers/auth_provider.dart';
class MockAuthNotifier extends Mock implements AuthNotifier {}
void main() {
testWidgets('LoginForm shows error when fields are empty', (tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(home: Scaffold(body: LoginForm())),
),
);
// النقر على إرسال دون ملء الحقول
await tester.tap(find.widgetWithText(ElevatedButton, 'Sign In'));
await tester.pump();
expect(find.text('Email is required'), findsOneWidget);
expect(find.text('Password is required'), findsOneWidget);
});
testWidgets('LoginForm calls signIn with entered credentials',
(tester) async {
String? capturedEmail;
String? capturedPassword;
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: Scaffold(
body: LoginForm(
onSubmit: (email, password) {
capturedEmail = email;
capturedPassword = password;
},
),
),
),
),
);
await tester.enterText(
find.byKey(const Key('emailField')), 'user@example.com');
await tester.enterText(
find.byKey(const Key('passwordField')), 'secret123');
await tester.tap(find.widgetWithText(ElevatedButton, 'Sign In'));
await tester.pump();
expect(capturedEmail, 'user@example.com');
expect(capturedPassword, 'secret123');
});
}
await tester.pumpAndSettle() بعد تشغيل الرسوم المتحركة أو التنقل. استخدم await tester.pump() لتقدم إطار واحد عندما تريد تحكماً دقيقاً في توقيت الأحداث غير المتزامنة.اختبارات التكامل — تدفق المصادقة والميزة الرئيسية مع GoRouter
تعمل اختبارات التكامل على جهاز أو محاكي حقيقي وتقود حزمة التطبيق الكاملة بما في ذلك التنقل. مع GoRouter، يكون الموجِّه نفسه جزءاً من شجرة الودجات، لذا تغلِّف التطبيق بـ ProviderScope حقيقي ومزودات حقيقية أو وهمية، ثم تحاكي رحلة المستخدم الكاملة.
ضع اختبارات التكامل في integration_test/ (وليس test/) واستخدم حزمة integration_test. شغِّلها بـ flutter test integration_test/ أو flutter drive.
اختبار التكامل: تدفق المصادقة الكامل + التنقل إلى الرئيسية
// integration_test/auth_flow_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:my_app/main.dart' as app;
import 'package:my_app/presentation/providers/auth_provider.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Authentication and main feature flow', () {
testWidgets('User can sign in and reach the home feed', (tester) async {
// تشغيل التطبيق الحقيقي
app.main();
await tester.pumpAndSettle();
// التوقع بالهبوط على شاشة تسجيل الدخول (إعادة توجيه GoRouter)
expect(find.text('Sign In'), findsOneWidget);
// ملء بيانات الاعتماد
await tester.enterText(
find.byKey(const Key('emailField')), 'demo@example.com');
await tester.enterText(
find.byKey(const Key('passwordField')), 'Demo1234!');
// الإرسال
await tester.tap(find.widgetWithText(ElevatedButton, 'Sign In'));
await tester.pumpAndSettle(const Duration(seconds: 3));
// يجب أن يعيد GoRouter التوجيه إلى /home بعد المصادقة الناجحة
expect(find.text('Welcome'), findsOneWidget);
expect(find.byType(BottomNavigationBar), findsOneWidget);
// التنقل إلى تبويب المهام
await tester.tap(find.byIcon(Icons.task_alt));
await tester.pumpAndSettle();
expect(find.text('My Tasks'), findsOneWidget);
});
testWidgets('Invalid credentials show error snackbar', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(
find.byKey(const Key('emailField')), 'bad@example.com');
await tester.enterText(
find.byKey(const Key('passwordField')), 'wrong');
await tester.tap(find.widgetWithText(ElevatedButton, 'Sign In'));
await tester.pumpAndSettle(const Duration(seconds: 3));
// يجب أن يبقى على تسجيل الدخول ويعرض رسالة خطأ
expect(find.text('Sign In'), findsOneWidget);
expect(find.byType(SnackBar), findsOneWidget);
});
});
}
test/ — يخبر مجلد integration_test/ Flutter باستخدام IntegrationTestWidgetsFlutterBinding بدلاً من الربط القياسي للاختبار.هيكلة مجموعة الاختبارات
يحتفظ مشروع Capstone المنظم بشكل جيد بالاختبارات التي تعكس شجرة المصدر:
test/domain/— اختبارات وحدة لحالات الاستخدام والكيانات وكائنات القيمةtest/data/— اختبارات وحدة لتنفيذات المستودعات (مع مصادر بيانات وهمية)test/presentation/— اختبارات ودجت للشاشات والودجات القابلة لإعادة الاستخدامintegration_test/— تدفقات من النهاية إلى النهاية تمارس الموجِّه الحقيقي والمزودات الحقيقية أو المزروعة
الخلاصة
تمنحك استراتيجية الاختبار ذات الثلاث طبقات في Flutter تغطية كاملة لتطبيق احترافي. اختبارات الوحدة تتحقق من منطق الأعمال بمعزل باستخدام المكوكات، وتعمل في ميلي ثوانٍ. اختبارات الودجت تمارس مكونات واجهة المستخدم بدون رأس، وتكتشف أخطاء العرض والتفاعل دون جهاز. اختبارات التكامل تقود التطبيق الحقيقي — بما في ذلك إعادة توجيهات GoRouter وحالة المزود — للتحقق من صحة رحلات المستخدم الكاملة. معاً تشكل شبكة أمان تتيح لمشروع Capstone التطور بثقة.