Testez logique et widgets avec des fakes écrits main, une horloge simulée et les bons réflexes de pump.
Ouvrir cette leçon dans KodokonLa pyramide de tests Flutter : une large base de tests unitaires Dart purs, une couche de tests de widgets, quelques tests d'intégration. Un test unitaire ne devrait jamais importer material.dart - si votre logique l'exige, le problème est architectural, pas technique. Pour isoler le code de ses dépendances, préférez les fakes écrits à la main aux mocks générés : un fake est une implémentation simplifiée mais réellement fonctionnelle du contrat (repository en mémoire, horloge pilotable) ; il survit aux refactorings et se lit comme du code normal. Les mocks, qui vérifient les interactions (« cette méthode a été appelée deux fois »), couplent le test aux détails d'implémentation - à réserver aux protocoles où l'interaction est le comportement.
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 recrée les objets avant chaque test : aucun état partagé, donc aucun test qui dépend de l'ordre d'exécution - c'est la condition de leur fiabilité. Les matchers composables (isA<Exception>(), throwsA, isNull) produisent des messages d'échec précis, bien plus utiles qu'un booléen. Remarquez le second test ci-dessous : il vérifie l'échec puis le rétablissement dans la foulée. Les chemins d'erreur sont du code comme les autres - s'ils ne sont pas testés, considérez-les comme faux.
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('réseau indisponible');
}
return 42;
}
}
void main() {
late FakeScoreRepository repository;
late ScoreController controller;
setUp(() {
repository = FakeScoreRepository();
controller = ScoreController(repository);
});
test('expose le meilleur score', () async {
await controller.load();
expect(controller.best, 42);
expect(controller.error, isNull);
expect(repository.calls, 1);
});
test('capture une erreur puis se rétablit', () async {
repository.failNextCall = true;
await controller.load();
expect(controller.error, isA<Exception>());
await controller.load();
expect(controller.best, 42);
expect(controller.error, isNull);
});
test('notifie ses écouteurs', () async {
var notifications = 0;
controller.addListener(() => notifications++);
await controller.load();
expect(notifications, 1);
});
}Un test de widget ne rend rien à l'écran : il tourne dans un binding spécialisé (AutomatedTestWidgetsFlutterBinding) où le temps est simulé et où chaque frame est produite à la demande. pumpWidget monte l'arbre ; pump() avance l'horloge factice et produit une frame ; pump(const Duration(milliseconds: 150)) saute dans le temps - idéal pour figer un état intermédiaire d'animation et l'inspecter. Les Finder (find.text, find.byType, find.byKey) interrogent l'arbre réel des Elements, pas une capture d'écran. Comme rien n'est asynchrone pour de vrai - ni horloge ni réseau -, ces tests sont déterministes : un échec est toujours reproductible.
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('Nouveau'),
IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
setState(() => _visible = false),
),
],
),
);
}
}
void main() {
testWidgets('masque le badge après le tap',
(tester) async {
await tester.pumpWidget(
const MaterialApp(home: BadgeScreen()),
);
expect(find.text('Nouveau'), findsOneWidget);
await tester.tap(find.byIcon(Icons.close));
await tester.pump();
expect(find.text('Nouveau'), findsNothing);
});
}flutter test
flutter test test/score_controller_test.dart
flutter test --coverage
flutter test --update-goldens