[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-test-end-to-end-in-flutter-con-integration-test-setup-robot-pattern-e-ci":3,"comments-tutorial-test-end-to-end-in-flutter-con-integration-test-setup-robot-pattern-e-ci":85},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":34},96,"Test end-to-end in Flutter con integration_test: setup, robot pattern e CI","test-end-to-end-in-flutter-con-integration-test-setup-robot-pattern-e-ci","Guida avanzata ai test end-to-end in Flutter: configurazione di integration_test, robot pattern, gestione di attese e stati asincroni, screenshot e esecuzione su CI.","I widget test verificano singoli componenti, ma non dicono nulla su come l'app si comporta **davvero** su un dispositivo, con il suo motore di rendering, i plugin nativi e la navigazione reale.\n\nIl pacchetto `integration_test`, mantenuto dal team Flutter, permette di scrivere test end-to-end che girano sull'app completa, su emulatore, device fisico o browser.\n\nIn questo tutorial avanzato vedremo:\n\n- come configurare `integration_test` in un progetto esistente;\n- come strutturare i test con il **robot pattern** per renderli leggibili e manutenibili;\n- come gestire attese, animazioni infinite e chiamate di rete mockate;\n- come raccogliere screenshot e metriche di performance;\n- come eseguire la suite in CI con GitHub Actions.\n\nRequisiti: conoscenza di `flutter_test`, `WidgetTester` e di un gestore di stato (useremo Riverpod, ma il concetto è identico con Provider o BLoC).","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F1be54482-37ad-4a6d-bd51-3577bea846b7.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1483817101829-339b08e8d83f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk1MzMxODl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Lewis Kang'ethe Ngugi","https:\u002F\u002Funsplash.com\u002F@ngeshlew","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fblack-laptop-computer-turned-on-f5pTwLHCsAg","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=a0XQUbL-PfY",{"youtube_id":17,"duration_seconds":18,"published_at":19},"a0XQUbL-PfY",139,"2026-09-16T09:38:06+00:00","advanced",45,"3.x","published","2026-09-16T04:33:09+00:00","Test end-to-end Flutter con integration_test","Tutorial avanzato su integration_test in Flutter: setup, robot pattern, mock delle dipendenze, screenshot e esecuzione della suite E2E su CI.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,51,57,64,71,78],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},640,"Configurare integration_test nel progetto","Aggiungi le dipendenze di sviluppo nel `pubspec.yaml`. `integration_test` è distribuito con l'SDK, quindi si usa `sdk: flutter`.\n\nCrea poi la cartella `integration_test\u002F` nella root del progetto (allo stesso livello di `lib\u002F` e `test\u002F`).\n\nUn dettaglio spesso dimenticato: `IntegrationTestWidgetsFlutterBinding.ensureInitialized()` va chiamato **come prima riga** di `main()`, altrimenti il binding di default impedisce l'esecuzione su device reale.","# pubspec.yaml\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  integration_test:\n    sdk: flutter\n  mocktail: ^1.0.0\n\n# integration_test\u002Fapp_test.dart\n# (scheletro iniziale)\n","yaml","`flutter pub get` completa senza errori e la cartella integration_test è pronta.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":49,"expected_result":50,"demo_url":42,"video_url":42},641,2,"Rendere l'app testabile: entry point parametrizzato","Un'app E2E va avviata dal test con le stesse dipendenze della produzione, ma con la possibilità di sostituirne alcune (client HTTP, storage, servizi nativi).\n\nEstrai da `main.dart` una funzione `bootstrap()` che accetta gli override. Con Riverpod si usa la lista `overrides` di `ProviderScope`; con `get_it` si registrano le istanze fake prima di `runApp`.\n\nQuesto passo è la chiave dell'intera architettura di test: senza un entry point iniettabile finirai per testare la rete reale, con test lenti e instabili.","\u002F\u002F lib\u002Fbootstrap.dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_riverpod\u002Fflutter_riverpod.dart';\nimport 'app.dart';\n\nFuture\u003Cvoid> bootstrap({List\u003COverride> overrides = const []}) async {\n  WidgetsFlutterBinding.ensureInitialized();\n  runApp(\n    ProviderScope(\n      overrides: overrides,\n      child: const MyApp(),\n    ),\n  );\n}\n\n\u002F\u002F lib\u002Fmain.dart\nFuture\u003Cvoid> main() => bootstrap();","dart","L'app si avvia normalmente e può essere lanciata anche dai test con dipendenze sostituite.",{"id":52,"position":28,"title":53,"body":54,"code_snippet":55,"code_language":49,"expected_result":56,"demo_url":42,"video_url":42},642,"Il primo test end-to-end","Crea `integration_test\u002Flogin_test.dart`. La struttura è quella di un widget test, ma con il binding di integrazione e l'app reale.\n\nPunti di attenzione:\n\n- usa `tester.pumpAndSettle()` dopo azioni che avviano animazioni o navigazione;\n- se l'app ha animazioni **infinite** (spinner, shimmer) `pumpAndSettle` va in timeout: usa `tester.pump(const Duration(milliseconds: 300))` in un ciclo, oppure disattiva le animazioni in modalità test;\n- preferisci `find.byKey` ai finder testuali: i testi cambiano con la localizzazione.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:integration_test\u002Fintegration_test.dart';\nimport 'package:myapp\u002Fbootstrap.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('login con credenziali valide porta alla home', (tester) async {\n    await bootstrap(overrides: [\u002F* fake repository *\u002F]);\n    await tester.pumpAndSettle();\n\n    await tester.enterText(find.byKey(const Key('email_field')), 'mario@test.it');\n    await tester.enterText(find.byKey(const Key('password_field')), 'password123');\n    await tester.testTextInput.receiveAction(TextInputAction.done);\n    await tester.pump();\n\n    await tester.tap(find.byKey(const Key('login_button')));\n    await tester.pumpAndSettle(const Duration(seconds: 3));\n\n    expect(find.byKey(const Key('home_screen')), findsOneWidget);\n  });\n}","Con `flutter test integration_test\u002Flogin_test.dart -d \u003Cdevice>` l'app si avvia sul device e il test passa.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":49,"expected_result":63,"demo_url":42,"video_url":42},643,4,"Organizzare i test con il robot pattern","Man mano che i flussi crescono, i test diventano muri di `find.byKey` illeggibili. Il **robot pattern** incapsula le interazioni con una schermata in una classe dedicata, esponendo metodi che parlano il linguaggio del dominio.\n\nVantaggi: se cambia una Key, si aggiorna un solo file; i test diventano documentazione leggibile anche dal product owner.\n\nConvenzione: un robot per schermata, con metodi di **azione** (`inserisciEmail`) e di **verifica** (`verificaErrore`).","class LoginRobot {\n  LoginRobot(this.tester);\n  final WidgetTester tester;\n\n  Future\u003Cvoid> inserisciCredenziali(String email, String pw) async {\n    await tester.enterText(find.byKey(const Key('email_field')), email);\n    await tester.enterText(find.byKey(const Key('password_field')), pw);\n    await tester.pump();\n  }\n\n  Future\u003Cvoid> premiAccedi() async {\n    await tester.tap(find.byKey(const Key('login_button')));\n    await tester.pumpAndSettle();\n  }\n\n  void verificaErrore(String messaggio) {\n    expect(find.text(messaggio), findsOneWidget);\n  }\n}\n\n\u002F\u002F Uso nel test\ntestWidgets('credenziali errate mostrano errore', (tester) async {\n  await bootstrap(overrides: [authOverrideChefallisce]);\n  await tester.pumpAndSettle();\n\n  final login = LoginRobot(tester);\n  await login.inserisciCredenziali('x@y.it', 'sbagliata');\n  await login.premiAccedi();\n  login.verificaErrore('Credenziali non valide');\n});","I test diventano una sequenza di frasi leggibili; le Key sono centralizzate nei robot.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":49,"expected_result":70,"demo_url":42,"video_url":42},644,5,"Gestire attese, scroll e stati asincroni instabili","I test E2E falliscono soprattutto per problemi di timing. Alcune tecniche consolidate:\n\n1. **Attesa condizionale**: invece di `pumpAndSettle` cieco, cicla il pump finché un finder non compare o scade un timeout.\n2. **Scroll fino all'elemento**: `tester.scrollUntilVisible` per liste lunghe con `ListView.builder`.\n3. **Animazioni disattivate**: esponi un flag di configurazione che imposta durate a `Duration.zero` in modalità test.\n4. Evita `Future.delayed` arbitrari nel codice di produzione: rendono i test lenti e fragili.","Future\u003Cvoid> waitFor(\n  WidgetTester tester,\n  Finder finder, {\n  Duration timeout = const Duration(seconds: 10),\n}) async {\n  final fine = DateTime.now().add(timeout);\n  while (DateTime.now().isBefore(fine)) {\n    await tester.pump(const Duration(milliseconds: 100));\n    if (finder.evaluate().isNotEmpty) return;\n  }\n  throw TestFailure('Timeout: $finder non trovato entro $timeout');\n}\n\n\u002F\u002F Scroll in una lista lunga\nawait tester.scrollUntilVisible(\n  find.byKey(const Key('item_87')),\n  300,\n  scrollable: find.byType(Scrollable).first,\n);","I test non vanno più in timeout su spinner o liste lunghe e diventano deterministici.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":49,"expected_result":77,"demo_url":42,"video_url":42},645,6,"Screenshot e metriche di performance","`IntegrationTestWidgetsFlutterBinding` permette di catturare screenshot (utili per la verifica visiva e per gli store) e di misurare il tempo di rendering dei frame con `traceAction`.\n\nSu Android serve `convertFlutterSurfaceToImage()` prima dello screenshot. Su iOS la chiamata non è necessaria (e va evitata).\n\nPer le performance, `reportData` viene scritto su file quando si usa `flutter drive`, permettendo analisi di jank su build profile.","late IntegrationTestWidgetsFlutterBinding binding;\n\nvoid main() {\n  binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('screenshot della home', (tester) async {\n    await bootstrap();\n    await tester.pumpAndSettle();\n\n    if (Platform.isAndroid) {\n      await binding.convertFlutterSurfaceToImage();\n      await tester.pumpAndSettle();\n    }\n    await binding.takeScreenshot('home');\n  });\n\n  testWidgets('scroll fluido della lista', (tester) async {\n    await bootstrap();\n    await tester.pumpAndSettle();\n\n    await binding.traceAction(() async {\n      await tester.fling(find.byType(ListView), const Offset(0, -500), 3000);\n      await tester.pumpAndSettle();\n    }, reportKey: 'scroll_timeline');\n  });\n}","Gli screenshot vengono salvati dal driver e il timeline dello scroll è disponibile per l'analisi del jank.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":40,"expected_result":84,"demo_url":42,"video_url":42},646,7,"Eseguire la suite in CI con GitHub Actions","L'ultimo tassello è l'automazione. Su GitHub Actions si usa un emulatore Android headless (`reactivecircus\u002Fandroid-emulator-runner`) e si lancia l'intera cartella `integration_test`.\n\nConsigli pratici:\n\n- crea `integration_test\u002Fall_tests.dart` che importa e invoca i `main()` dei singoli file: riduce i riavvii dell'app e accorcia molto i tempi;\n- fissa una versione di API level stabile (33\u002F34) e abilita la cache dell'AVD;\n- marca il job come required sulla PR solo quando la suite è stabile, altrimenti diventa rumore.","name: e2e\non: [pull_request]\n\njobs:\n  android:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-java@v4\n        with:\n          distribution: 'zulu'\n          java-version: '17'\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.0'\n      - run: flutter pub get\n      - name: Run integration tests\n        uses: reactivecircus\u002Fandroid-emulator-runner@v2\n        with:\n          api-level: 34\n          arch: x86_64\n          profile: pixel_6\n          script: flutter test integration_test\u002Fall_tests.dart","Ad ogni pull request la suite E2E gira su emulatore e blocca il merge in caso di regressioni sui flussi critici.",[],1792316930852]