[{"data":1,"prerenderedAt":748},["ShallowReactive",2],{"article-\u002Fblog\u002Ftester-une-application-flutter-strategie-complete":3},{"id":4,"title":5,"body":6,"brouillon":735,"date":736,"description":737,"extension":738,"image":739,"imageAlt":739,"meta":740,"navigation":73,"path":741,"seo":742,"stem":743,"tags":744,"__hash__":747},"blog\u002Fblog\u002Ftester-une-application-flutter-strategie-complete.md","Tester une application Flutter : la stratégie complète, de mocktail aux widget tests",{"type":7,"value":8,"toc":729},"minimark",[9,13,18,21,29,353,375,379,386,560,563,567,570,715,718,722,725],[10,11,12],"p",{},"Le test en Flutter souffre d'un malentendu : on pense « tester l'interface »\nalors que l'essentiel de la valeur est ailleurs. Voici la stratégie que\nj'applique (une pyramide dont chaque étage a un rôle précis) et le rôle\ncentral qu'y joue mocktail.",[14,15,17],"h2",{"id":16},"létage-1-tests-unitaires-du-domaine","L'étage 1 : tests unitaires du domaine",[10,19,20],{},"Le socle : tester la logique métier pure (use cases, validations,\ntransformations) sans Flutter, sans réseau, sans base. C'est ici que la\nClean Architecture paie : un use case qui dépend d'une interface de\nrepository se teste avec un mock.",[10,22,23,24,28],{},"Pour les mocks, ",[25,26,27],"strong",{},"mocktail"," (le successeur spirituel de mockito, sans\ngénération de code) :",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-dart shiki shiki-themes github-dark","class MockAuthRepository extends Mock implements AuthRepository {}\n\nvoid main() {\n  late MockAuthRepository repository;\n  late VerifyOtpUseCase useCase;\n\n  setUp(() {\n    repository = MockAuthRepository();\n    useCase = VerifyOtpUseCase(repository);\n  });\n\n  test('retourne un échec quand le code OTP est expiré', () async {\n    when(() => repository.verifyOtp(any(), any()))\n        .thenAnswer((_) async => const Left(OtpExpiredFailure()));\n\n    final result = await useCase('+242066000000', '123456');\n\n    expect(result, const Left(OtpExpiredFailure()));\n    verify(() => repository.verifyOtp(any(), any())).called(1);\n  });\n}\n","dart","",[37,38,39,68,75,88,99,110,115,124,138,151,157,162,184,215,246,251,282,287,307,342,347],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58,61,64],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"snl16","class",[40,49,51],{"class":50},"sDLfK"," MockAuthRepository",[40,53,54],{"class":46}," extends",[40,56,57],{"class":50}," Mock",[40,59,60],{"class":46}," implements",[40,62,63],{"class":50}," AuthRepository",[40,65,67],{"class":66},"s95oV"," {}\n",[40,69,71],{"class":42,"line":70},2,[40,72,74],{"emptyLinePlaceholder":73},true,"\n",[40,76,78,81,85],{"class":42,"line":77},3,[40,79,80],{"class":46},"void",[40,82,84],{"class":83},"svObZ"," main",[40,86,87],{"class":66},"() {\n",[40,89,91,94,96],{"class":42,"line":90},4,[40,92,93],{"class":46},"  late",[40,95,51],{"class":50},[40,97,98],{"class":66}," repository;\n",[40,100,102,104,107],{"class":42,"line":101},5,[40,103,93],{"class":46},[40,105,106],{"class":50}," VerifyOtpUseCase",[40,108,109],{"class":66}," useCase;\n",[40,111,113],{"class":42,"line":112},6,[40,114,74],{"emptyLinePlaceholder":73},[40,116,118,121],{"class":42,"line":117},7,[40,119,120],{"class":83},"  setUp",[40,122,123],{"class":66},"(() {\n",[40,125,127,130,133,135],{"class":42,"line":126},8,[40,128,129],{"class":66},"    repository ",[40,131,132],{"class":46},"=",[40,134,51],{"class":50},[40,136,137],{"class":66},"();\n",[40,139,141,144,146,148],{"class":42,"line":140},9,[40,142,143],{"class":66},"    useCase ",[40,145,132],{"class":46},[40,147,106],{"class":50},[40,149,150],{"class":66},"(repository);\n",[40,152,154],{"class":42,"line":153},10,[40,155,156],{"class":66},"  });\n",[40,158,160],{"class":42,"line":159},11,[40,161,74],{"emptyLinePlaceholder":73},[40,163,165,168,171,175,178,181],{"class":42,"line":164},12,[40,166,167],{"class":83},"  test",[40,169,170],{"class":66},"(",[40,172,174],{"class":173},"sU2Wk","'retourne un échec quand le code OTP est expiré'",[40,176,177],{"class":66},", () ",[40,179,180],{"class":46},"async",[40,182,183],{"class":66}," {\n",[40,185,187,190,193,196,199,202,204,207,210,212],{"class":42,"line":186},13,[40,188,189],{"class":46},"    when",[40,191,192],{"class":66},"(() ",[40,194,195],{"class":46},"=>",[40,197,198],{"class":66}," repository.",[40,200,201],{"class":83},"verifyOtp",[40,203,170],{"class":66},[40,205,206],{"class":83},"any",[40,208,209],{"class":66},"(), ",[40,211,206],{"class":83},[40,213,214],{"class":66},"()))\n",[40,216,218,221,224,227,229,232,235,238,240,243],{"class":42,"line":217},14,[40,219,220],{"class":66},"        .",[40,222,223],{"class":83},"thenAnswer",[40,225,226],{"class":66},"((_) ",[40,228,180],{"class":46},[40,230,231],{"class":46}," =>",[40,233,234],{"class":46}," const",[40,236,237],{"class":50}," Left",[40,239,170],{"class":66},[40,241,242],{"class":50},"OtpExpiredFailure",[40,244,245],{"class":66},"()));\n",[40,247,249],{"class":42,"line":248},15,[40,250,74],{"emptyLinePlaceholder":73},[40,252,254,257,260,262,265,268,270,273,276,279],{"class":42,"line":253},16,[40,255,256],{"class":46},"    final",[40,258,259],{"class":66}," result ",[40,261,132],{"class":46},[40,263,264],{"class":46}," await",[40,266,267],{"class":83}," useCase",[40,269,170],{"class":66},[40,271,272],{"class":173},"'+242066000000'",[40,274,275],{"class":66},", ",[40,277,278],{"class":173},"'123456'",[40,280,281],{"class":66},");\n",[40,283,285],{"class":42,"line":284},17,[40,286,74],{"emptyLinePlaceholder":73},[40,288,290,293,296,299,301,303,305],{"class":42,"line":289},18,[40,291,292],{"class":83},"    expect",[40,294,295],{"class":66},"(result, ",[40,297,298],{"class":46},"const",[40,300,237],{"class":50},[40,302,170],{"class":66},[40,304,242],{"class":50},[40,306,245],{"class":66},[40,308,310,313,315,317,319,321,323,325,327,329,332,335,337,340],{"class":42,"line":309},19,[40,311,312],{"class":83},"    verify",[40,314,192],{"class":66},[40,316,195],{"class":46},[40,318,198],{"class":66},[40,320,201],{"class":83},[40,322,170],{"class":66},[40,324,206],{"class":83},[40,326,209],{"class":66},[40,328,206],{"class":83},[40,330,331],{"class":66},"())).",[40,333,334],{"class":83},"called",[40,336,170],{"class":66},[40,338,339],{"class":50},"1",[40,341,281],{"class":66},[40,343,345],{"class":42,"line":344},20,[40,346,156],{"class":66},[40,348,350],{"class":42,"line":349},21,[40,351,352],{"class":66},"}\n",[10,354,355,356,358,359,362,363,366,367,370,371,374],{},"Pas de build_runner, pas de fichiers générés : ",[37,357,27],{}," crée le mock par\nhéritage, et ",[37,360,361],{},"when","\u002F",[37,364,365],{},"verify"," pilotent le comportement. Seule subtilité :\nenregistrer les fallback values pour les types personnalisés passés à\n",[37,368,369],{},"any()"," (",[37,372,373],{},"registerFallbackValue","). C'est le piège classique du débutant.",[14,376,378],{"id":377},"létage-2-tests-de-blocs","L'étage 2 : tests de blocs",[10,380,381,382,385],{},"Les blocs\u002Fcubits sont la logique de présentation ; le package ",[37,383,384],{},"bloc_test","\nles teste comme des scénarios :",[30,387,389],{"className":32,"code":388,"language":34,"meta":35,"style":35},"blocTest\u003COtpCubit, OtpState>(\n  'émet [loading, failure] quand le code est invalide',\n  build: () {\n    when(() => useCase(any(), any()))\n        .thenAnswer((_) async => const Left(InvalidOtpFailure()));\n    return OtpCubit(useCase);\n  },\n  act: (cubit) => cubit.verify('+242066000000', '000000'),\n  expect: () => [OtpState.loading(), OtpState.failure(InvalidOtpFailure())],\n);\n",[37,390,391,410,418,429,449,472,483,488,517,556],{"__ignoreMap":35},[40,392,393,396,399,402,404,407],{"class":42,"line":43},[40,394,395],{"class":83},"blocTest",[40,397,398],{"class":66},"\u003C",[40,400,401],{"class":50},"OtpCubit",[40,403,275],{"class":66},[40,405,406],{"class":50},"OtpState",[40,408,409],{"class":66},">(\n",[40,411,412,415],{"class":42,"line":70},[40,413,414],{"class":173},"  'émet [loading, failure] quand le code est invalide'",[40,416,417],{"class":66},",\n",[40,419,420,423,426],{"class":42,"line":77},[40,421,422],{"class":66},"  build",[40,424,425],{"class":46},":",[40,427,428],{"class":66}," () {\n",[40,430,431,433,435,437,439,441,443,445,447],{"class":42,"line":90},[40,432,189],{"class":46},[40,434,192],{"class":66},[40,436,195],{"class":46},[40,438,267],{"class":83},[40,440,170],{"class":66},[40,442,206],{"class":83},[40,444,209],{"class":66},[40,446,206],{"class":83},[40,448,214],{"class":66},[40,450,451,453,455,457,459,461,463,465,467,470],{"class":42,"line":101},[40,452,220],{"class":66},[40,454,223],{"class":83},[40,456,226],{"class":66},[40,458,180],{"class":46},[40,460,231],{"class":46},[40,462,234],{"class":46},[40,464,237],{"class":50},[40,466,170],{"class":66},[40,468,469],{"class":50},"InvalidOtpFailure",[40,471,245],{"class":66},[40,473,474,477,480],{"class":42,"line":112},[40,475,476],{"class":46},"    return",[40,478,479],{"class":50}," OtpCubit",[40,481,482],{"class":66},"(useCase);\n",[40,484,485],{"class":42,"line":117},[40,486,487],{"class":66},"  },\n",[40,489,490,493,495,498,500,503,505,507,509,511,514],{"class":42,"line":126},[40,491,492],{"class":66},"  act",[40,494,425],{"class":46},[40,496,497],{"class":66}," (cubit) ",[40,499,195],{"class":46},[40,501,502],{"class":66}," cubit.",[40,504,365],{"class":83},[40,506,170],{"class":66},[40,508,272],{"class":173},[40,510,275],{"class":66},[40,512,513],{"class":173},"'000000'",[40,515,516],{"class":66},"),\n",[40,518,519,522,524,527,529,532,534,537,540,542,544,546,549,551,553],{"class":42,"line":140},[40,520,521],{"class":66},"  expect",[40,523,425],{"class":46},[40,525,526],{"class":66}," () ",[40,528,195],{"class":46},[40,530,531],{"class":66}," [",[40,533,406],{"class":50},[40,535,536],{"class":66},".",[40,538,539],{"class":83},"loading",[40,541,209],{"class":66},[40,543,406],{"class":50},[40,545,536],{"class":66},[40,547,548],{"class":83},"failure",[40,550,170],{"class":66},[40,552,469],{"class":50},[40,554,555],{"class":66},"())],\n",[40,557,558],{"class":42,"line":153},[40,559,281],{"class":66},[10,561,562],{},"Ces tests sont rapides, lisibles, et documentent les transitions d'état\nmieux que n'importe quel commentaire. C'est l'étage au meilleur rapport\nprotection\u002Feffort : si vous ne testez qu'une chose, testez ici.",[14,564,566],{"id":565},"létage-3-widget-tests-avec-parcimonie","L'étage 3 : widget tests, avec parcimonie",[10,568,569],{},"Les widget tests vérifient qu'un écran réagit correctement à un état donné :\non injecte un bloc mocké, on pompe le widget, on vérifie le rendu.",[30,571,573],{"className":32,"code":572,"language":34,"meta":35,"style":35},"testWidgets('affiche le message d\\'erreur en cas d\\'échec', (tester) async {\n  whenListen(mockCubit, Stream.value(OtpState.failure(InvalidOtpFailure())),\n      initialState: OtpState.initial());\n  await tester.pumpWidget(makeTestable(OtpScreen(cubit: mockCubit)));\n  await tester.pump();\n  expect(find.text('Code invalide'), findsOneWidget);\n});\n",[37,574,575,603,634,652,681,692,710],{"__ignoreMap":35},[40,576,577,580,582,585,588,591,593,596,599,601],{"class":42,"line":43},[40,578,579],{"class":83},"testWidgets",[40,581,170],{"class":66},[40,583,584],{"class":173},"'affiche le message d",[40,586,587],{"class":50},"\\'",[40,589,590],{"class":173},"erreur en cas d",[40,592,587],{"class":50},[40,594,595],{"class":173},"échec'",[40,597,598],{"class":66},", (tester) ",[40,600,180],{"class":46},[40,602,183],{"class":66},[40,604,605,608,611,614,616,619,621,623,625,627,629,631],{"class":42,"line":70},[40,606,607],{"class":83},"  whenListen",[40,609,610],{"class":66},"(mockCubit, ",[40,612,613],{"class":50},"Stream",[40,615,536],{"class":66},[40,617,618],{"class":83},"value",[40,620,170],{"class":66},[40,622,406],{"class":50},[40,624,536],{"class":66},[40,626,548],{"class":83},[40,628,170],{"class":66},[40,630,469],{"class":50},[40,632,633],{"class":66},"())),\n",[40,635,636,639,641,644,646,649],{"class":42,"line":77},[40,637,638],{"class":66},"      initialState",[40,640,425],{"class":46},[40,642,643],{"class":50}," OtpState",[40,645,536],{"class":66},[40,647,648],{"class":83},"initial",[40,650,651],{"class":66},"());\n",[40,653,654,657,660,663,665,668,670,673,676,678],{"class":42,"line":90},[40,655,656],{"class":46},"  await",[40,658,659],{"class":66}," tester.",[40,661,662],{"class":83},"pumpWidget",[40,664,170],{"class":66},[40,666,667],{"class":83},"makeTestable",[40,669,170],{"class":66},[40,671,672],{"class":50},"OtpScreen",[40,674,675],{"class":66},"(cubit",[40,677,425],{"class":46},[40,679,680],{"class":66}," mockCubit)));\n",[40,682,683,685,687,690],{"class":42,"line":101},[40,684,656],{"class":46},[40,686,659],{"class":66},[40,688,689],{"class":83},"pump",[40,691,137],{"class":66},[40,693,694,696,699,702,704,707],{"class":42,"line":112},[40,695,521],{"class":83},[40,697,698],{"class":66},"(find.",[40,700,701],{"class":83},"text",[40,703,170],{"class":66},[40,705,706],{"class":173},"'Code invalide'",[40,708,709],{"class":66},"), findsOneWidget);\n",[40,711,712],{"class":42,"line":117},[40,713,714],{"class":66},"});\n",[10,716,717],{},"Mon conseil : réservez-les aux écrans critiques (auth, paiement, saisie de\ndonnées importantes) et aux composants réutilisés partout. Tester chaque\nécran pixel par pixel coûte cher en maintenance pour une protection\nmarginale. C'est la partie de la pyramide qu'on regrette d'avoir trop\nconstruite.",[14,719,721],{"id":720},"la-leçon","La leçon",[10,723,724],{},"La bonne pyramide Flutter : beaucoup de tests de domaine, une couverture\nsystématique des blocs, quelques widget tests ciblés. Et une règle\nd'hygiène : chaque bug corrigé gagne son test de non-régression, c'est la\nseule couverture qui croît exactement là où votre application casse\nvraiment.",[726,727,728],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":35,"searchDepth":70,"depth":70,"links":730},[731,732,733,734],{"id":16,"depth":70,"text":17},{"id":377,"depth":70,"text":378},{"id":565,"depth":70,"text":566},{"id":720,"depth":70,"text":721},false,"2026-06-23","Tests unitaires, tests de blocs, widget tests : comment structurer une pyramide de tests Flutter qui protège vraiment, sans ralentir le développement.","md",null,{},"\u002Fblog\u002Ftester-une-application-flutter-strategie-complete",{"title":5,"description":737},"blog\u002Ftester-une-application-flutter-strategie-complete",[745,746],"Flutter","Tests","XlLEaWC-cR7xQ2oKP2lNwKjnGHIgOv1t9o_c97sbupY",1784652016702]