हाथ से लिखे फ़ेक, एक सिम्युलेटेड क्लॉक और सही pump सहज-आदतों के साथ लॉजिक और विजेट का परीक्षण करें।
इस पाठ को Kodokon में खोलेंFlutter का टेस्ट पिरामिड: शुद्ध Dart यूनिट टेस्ट का एक चौड़ा आधार, विजेट टेस्ट की एक परत, कुछ इंटीग्रेशन टेस्ट। एक यूनिट टेस्ट को कभी भी material.dart इम्पोर्ट नहीं करना चाहिए - यदि आपके लॉजिक को इसकी ज़रूरत पड़ती है, तो समस्या तकनीकी नहीं, बल्कि आर्किटेक्चरल है। कोड को उसकी निर्भरताओं से पृथक करने के लिए, जेनरेट किए गए mocks के बजाय हाथ से लिखे फ़ेक को प्राथमिकता दें: एक फ़ेक अनुबंध का एक सरलीकृत पर वास्तव में कार्यात्मक इम्प्लीमेंटेशन होता है (इन-मेमोरी रिपॉज़िटरी, नियंत्रणीय क्लॉक); यह रीफ़ैक्टरिंग को झेल जाता है और सामान्य कोड की तरह पढ़ा जाता है। 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, हर टेस्ट से पहले ऑब्जेक्ट को दोबारा बनाता है: कोई साझा स्टेट नहीं, इसलिए कोई ऐसा टेस्ट नहीं जो निष्पादन के क्रम पर निर्भर करे - यही उनकी विश्वसनीयता की शर्त है। संयोजनीय matchers (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) किसी स्क्रीनशॉट को नहीं, बल्कि Elements के असली ट्री को क्वेरी करते हैं। चूँकि कुछ भी सचमुच एसिंक्रोनस नहीं है - न क्लॉक, न नेटवर्क - इसलिए ये टेस्ट नियतात्मक (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