اختبر المنطق والودجات باستخدام نُسَخ زائفة (fakes) مكتوبة يدويًا، وساعة مُحاكاة، وردود الفعل الصحيحة تجاه pump.
افتح هذا الدرس في Kodokonهرم اختبارات Flutter: قاعدة عريضة من الاختبارات الوحدوية بلغة Dart الخالصة، وطبقة من اختبارات الودجات، وقليل من اختبارات التكامل. لا ينبغي لاختبار وحدوي أن يستورد أبدًا material.dart - فإن تطلّبه منطقك، فالمشكلة معمارية لا تقنية. ولعزل الكود عن تبعياته، فضِّل النُسَخ الزائفة (fakes) المكتوبة يدويًا على المحاكيات (mocks) المُولَّدة: فالنسخة الزائفة تنفيذ مبسّط لكنه وظيفي فعلًا للعقد (مستودع في الذاكرة، ساعة قابلة للتحكم)؛ وهي تصمد أمام عمليات إعادة الهيكلة وتُقرأ ككود عادي. أما المحاكيات، التي تتحقق من التفاعلات ("استُدعيت هذه الدالة مرتين")، فتربط الاختبار بتفاصيل التنفيذ - فاحتفظ بها للبروتوكولات التي يكون فيها التفاعل هو السلوك.
import 'package:flutter/foundation.dart';
abstract interface class ScoreRepository {
Future<int> fetchBest();
}
class ScoreController extends ChangeNotifier {
ScoreController(this._repository);
final ScoreRepository _repository;
int? best;
Object? error;
Future<void> load() async {
try {
best = await _repository.fetchBest();
error = null;
} on Exception catch (e) {
error = e;
}
notifyListeners();
}
}يعيد setUp إنشاء الكائنات قبل كل اختبار: فلا حالة مشتركة، وبالتالي لا اختبار يعتمد على ترتيب التنفيذ - وذلك شرط موثوقيتها. وتنتج أدوات المطابقة القابلة للتركيب (isA<Exception>()، throwsA، isNull) رسائل فشل دقيقة، أكثر فائدة بكثير من قيمة منطقية. لاحظ الاختبار الثاني أدناه: فهو يتحقق من الفشل ثم من التعافي مباشرةً بعده. فمسارات الخطأ كود مثل أيّ كود آخر - وإن لم تُختبَر، فاعتبرها معطّلة.
import 'package:flutter_test/flutter_test.dart';
class FakeScoreRepository implements ScoreRepository {
int calls = 0;
bool failNextCall = false;
@override
Future<int> fetchBest() async {
calls++;
if (failNextCall) {
failNextCall = false;
throw Exception('network unavailable');
}
return 42;
}
}
void main() {
late FakeScoreRepository repository;
late ScoreController controller;
setUp(() {
repository = FakeScoreRepository();
controller = ScoreController(repository);
});
test('exposes the best score', () async {
await controller.load();
expect(controller.best, 42);
expect(controller.error, isNull);
expect(repository.calls, 1);
});
test('captures an error then recovers', () async {
repository.failNextCall = true;
await controller.load();
expect(controller.error, isA<Exception>());
await controller.load();
expect(controller.best, 42);
expect(controller.error, isNull);
});
test('notifies its listeners', () async {
var notifications = 0;
controller.addListener(() => notifications++);
await controller.load();
expect(notifications, 1);
});
}لا يعرض اختبار الودجت أيّ شيء على الشاشة: فهو يعمل في ربط (binding) متخصّص (AutomatedTestWidgetsFlutterBinding) حيث يُحاكَى الزمن وحيث يُنتَج كل إطار عند الطلب. يركّب pumpWidget الشجرة؛ ويُقدّم pump() الساعة الزائفة ويُنتِج إطارًا واحدًا؛ ويقفز pump(const Duration(milliseconds: 150)) في الزمن - وهو مثالي لتجميد حالة حركة وسيطة وفحصها. وتستعلم أدوات Finder (find.text، find.byType، find.byKey) عن شجرة العناصر الحقيقية، لا عن لقطة شاشة. ولأنه لا شيء غير متزامن فعلًا - لا الساعة ولا الشبكة - فإن هذه الاختبارات حتمية (deterministic): فالفشل قابل لإعادة الإنتاج دائمًا.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class BadgeScreen extends StatefulWidget {
const BadgeScreen({super.key});
@override
State<BadgeScreen> createState() => _BadgeScreenState();
}
class _BadgeScreenState extends State<BadgeScreen> {
bool _visible = true;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
if (_visible) const Text('New'),
IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
setState(() => _visible = false),
),
],
),
);
}
}
void main() {
testWidgets('hides the badge after the tap',
(tester) async {
await tester.pumpWidget(
const MaterialApp(home: BadgeScreen()),
);
expect(find.text('New'), findsOneWidget);
await tester.tap(find.byIcon(Icons.close));
await tester.pump();
expect(find.text('New'), findsNothing);
});
}flutter test
flutter test test/score_controller_test.dart
flutter test --coverage
flutter test --update-goldens