23 juin 2026 · Flutter · Tests
Tester une application Flutter : la stratégie complète, de mocktail aux widget tests
Tests unitaires, tests de blocs, widget tests : comment structurer une pyramide de tests Flutter qui protège vraiment, sans ralentir le développement.
Le test en Flutter souffre d'un malentendu : on pense « tester l'interface » alors que l'essentiel de la valeur est ailleurs. Voici la stratégie que j'applique (une pyramide dont chaque étage a un rôle précis) et le rôle central qu'y joue mocktail.
L'étage 1 : tests unitaires du domaine
Le socle : tester la logique métier pure (use cases, validations, transformations) sans Flutter, sans réseau, sans base. C'est ici que la Clean Architecture paie : un use case qui dépend d'une interface de repository se teste avec un mock.
Pour les mocks, mocktail (le successeur spirituel de mockito, sans génération de code) :
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
late MockAuthRepository repository;
late VerifyOtpUseCase useCase;
setUp(() {
repository = MockAuthRepository();
useCase = VerifyOtpUseCase(repository);
});
test('retourne un échec quand le code OTP est expiré', () async {
when(() => repository.verifyOtp(any(), any()))
.thenAnswer((_) async => const Left(OtpExpiredFailure()));
final result = await useCase('+242066000000', '123456');
expect(result, const Left(OtpExpiredFailure()));
verify(() => repository.verifyOtp(any(), any())).called(1);
});
}
Pas de build_runner, pas de fichiers générés : mocktail crée le mock par
héritage, et when/verify pilotent le comportement. Seule subtilité :
enregistrer les fallback values pour les types personnalisés passés à
any() (registerFallbackValue). C'est le piège classique du débutant.
L'étage 2 : tests de blocs
Les blocs/cubits sont la logique de présentation ; le package bloc_test
les teste comme des scénarios :
blocTest<OtpCubit, OtpState>(
'émet [loading, failure] quand le code est invalide',
build: () {
when(() => useCase(any(), any()))
.thenAnswer((_) async => const Left(InvalidOtpFailure()));
return OtpCubit(useCase);
},
act: (cubit) => cubit.verify('+242066000000', '000000'),
expect: () => [OtpState.loading(), OtpState.failure(InvalidOtpFailure())],
);
Ces tests sont rapides, lisibles, et documentent les transitions d'état mieux que n'importe quel commentaire. C'est l'étage au meilleur rapport protection/effort : si vous ne testez qu'une chose, testez ici.
L'étage 3 : widget tests, avec parcimonie
Les widget tests vérifient qu'un écran réagit correctement à un état donné : on injecte un bloc mocké, on pompe le widget, on vérifie le rendu.
testWidgets('affiche le message d\'erreur en cas d\'échec', (tester) async {
whenListen(mockCubit, Stream.value(OtpState.failure(InvalidOtpFailure())),
initialState: OtpState.initial());
await tester.pumpWidget(makeTestable(OtpScreen(cubit: mockCubit)));
await tester.pump();
expect(find.text('Code invalide'), findsOneWidget);
});
Mon conseil : réservez-les aux écrans critiques (auth, paiement, saisie de données importantes) et aux composants réutilisés partout. Tester chaque écran pixel par pixel coûte cher en maintenance pour une protection marginale. C'est la partie de la pyramide qu'on regrette d'avoir trop construite.
La leçon
La bonne pyramide Flutter : beaucoup de tests de domaine, une couverture systématique des blocs, quelques widget tests ciblés. Et une règle d'hygiène : chaque bug corrigé gagne son test de non-régression, c'est la seule couverture qui croît exactement là où votre application casse vraiment.