[{"data":1,"prerenderedAt":78},["ShallowReactive",2],{"tutorial-test-end-to-end-deterministici-in-flutter-integration-test-con-backend-finto-e-tempo-controllato":3,"comments-tutorial-test-end-to-end-deterministici-in-flutter-integration-test-con-backend-finto-e-tempo-controllato":77},{"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":19,"estimated_minutes":20,"flutter_version":21,"status":22,"published_at":23,"meta_title":24,"meta_description":25,"category":26,"author":30,"steps":33},133,"Test end-to-end deterministici in Flutter: integration_test con backend finto e tempo controllato","test-end-to-end-deterministici-in-flutter-integration-test-con-backend-finto-e-tempo-controllato","Elimina i test E2E flaky: server HTTP locale al posto del backend reale, orologio controllato, deep link simulati e helper di attesa robusti con integration_test.","I test end-to-end falliscono raramente per un bug reale: molto più spesso per **non determinismo**. Rete lenta, animazioni infinite, date che cambiano, token scaduti.\n\nIn questo tutorial avanzato costruiamo una suite `integration_test` **ermetica**: l'app gira sul device reale ma parla con un server HTTP avviato nel processo di test, usa un orologio fisso e attese basate su condizioni invece che su `Future.delayed`.\n\n> Prerequisiti: conoscenza di `integration_test`, dependency injection e `HttpServer` di `dart:io`.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F9d70f62b-562d-4138-a09a-cdacf10f2c47.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1617040619263-41c5a9ca7521?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTM0MjEyMTR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Artur Shamsutdinov","https:\u002F\u002Funsplash.com\u002F@roketpik","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftext-Gll-v69L8iA","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=Z2JGCnZlu-c",{"youtube_id":17,"duration_seconds":4,"published_at":18},"Z2JGCnZlu-c","2026-10-31T10:42:47+00:00","advanced",45,"3.x","published","2026-10-31T04:33:34+00:00","Test E2E deterministici in Flutter con integration_test","Guida avanzata a integration_test: backend finto in-process, clock controllato, deep link e attese robuste per test end-to-end Flutter non flaky.",{"id":27,"name":28,"slug":29},3,"Best practice","best-practice",{"id":31,"name":32},1,"Flutter Bot",[34,42,49,55,62,69],{"id":35,"position":31,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},890,"Rendere l'app configurabile dall'esterno","Il primo requisito di un test ermetico è poter **iniettare** baseUrl, client HTTP e sorgente del tempo. Definiamo un `AppConfig` immutabile e un entry point parametrico, separato da `main()`.\n\nCosì il test non modifica il codice di produzione: lo *compone* diversamente.","\u002F\u002F lib\u002Fapp\u002Fapp_config.dart\nclass AppConfig {\n  final Uri baseUrl;\n  final DateTime Function() now;\n  final Duration animationScale;\n\n  const AppConfig({\n    required this.baseUrl,\n    required this.now,\n    this.animationScale = const Duration(milliseconds: 300),\n  });\n}\n\n\u002F\u002F lib\u002Fapp\u002Fbootstrap.dart\nWidget buildApp(AppConfig config) {\n  return ProviderScope(\n    overrides: [appConfigProvider.overrideWithValue(config)],\n    child: const MyApp(),\n  );\n}\n\n\u002F\u002F lib\u002Fmain.dart\nvoid main() => runApp(buildApp(AppConfig(\n      baseUrl: Uri.parse('https:\u002F\u002Fapi.example.com'),\n      now: DateTime.now,\n    )));","dart","L'app in produzione funziona come prima, ma è ora istanziabile con una configurazione arbitraria.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},891,2,"Avviare un backend finto in-process","Invece di mockare il client HTTP (che non testa serializzazione e header reali), avviamo un vero `HttpServer` su `127.0.0.1:0`. Il device e il test girano nello stesso processo con `integration_test`, quindi il loopback funziona su Android, iOS e desktop.\n\nIl server espone un **router minimale** e permette di programmare risposte per scenario (200, 401, 500).","\u002F\u002F integration_test\u002Fsupport\u002Ffake_backend.dart\nimport 'dart:convert';\nimport 'dart:io';\n\nclass FakeBackend {\n  late HttpServer _server;\n  final Map\u003CString, dynamic Function(HttpRequest)> routes = {};\n  final List\u003CString> calls = [];\n\n  Uri get baseUrl => Uri.parse('http:\u002F\u002F127.0.0.1:${_server.port}');\n\n  Future\u003Cvoid> start() async {\n    _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);\n    _server.listen((req) async {\n      final key = '${req.method} ${req.uri.path}';\n      calls.add(key);\n      final handler = routes[key];\n      if (handler == null) {\n        req.response.statusCode = 404;\n      } else {\n        final body = handler(req);\n        req.response\n          ..statusCode = 200\n          ..headers.contentType = ContentType.json\n          ..write(jsonEncode(body));\n      }\n      await req.response.close();\n    });\n  }\n\n  Future\u003Cvoid> stop() => _server.close(force: true);\n}","Un server locale su porta effimera, con registro delle chiamate ricevute per le asserzioni.",{"id":50,"position":27,"title":51,"body":52,"code_snippet":53,"code_language":39,"expected_result":54,"demo_url":41,"video_url":41},892,"Congelare il tempo e disattivare le animazioni infinite","Due classiche cause di flakiness:\n\n1. **Date relative** (\"2 ore fa\") che cambiano tra un run e l'altro.\n2. **Animazioni che non si fermano mai** (shimmer, spinner), che fanno andare `pumpAndSettle()` in timeout.\n\nUsiamo un clock fisso e, nei test, sostituiamo i loader infiniti con un widget statico tramite il flag di configurazione.","\u002F\u002F integration_test\u002Fsupport\u002Ftest_clock.dart\nclass TestClock {\n  DateTime _now = DateTime.utc(2024, 3, 15, 10, 0);\n  DateTime call() => _now;\n  void advance(Duration d) => _now = _now.add(d);\n}\n\n\u002F\u002F Nel widget di caricamento\nclass AppLoader extends ConsumerWidget {\n  const AppLoader({super.key});\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final scale = ref.watch(appConfigProvider).animationScale;\n    if (scale == Duration.zero) {\n      return const SizedBox(key: Key('loader'), height: 24, width: 24);\n    }\n    return const CircularProgressIndicator(key: Key('loader'));\n  }\n}","Con `animationScale: Duration.zero` nessuna animazione perpetua blocca `pumpAndSettle`.",{"id":56,"position":57,"title":58,"body":59,"code_snippet":60,"code_language":39,"expected_result":61,"demo_url":41,"video_url":41},893,4,"Attese robuste: pumpUntilFound invece di delay fissi","`await tester.pump(const Duration(seconds: 2))` è una scommessa. Meglio una helper che pompa frame finché una condizione non è vera, con timeout esplicito e messaggio d'errore utile.\n\nAggiungiamo anche un piccolo **robot** per leggibilità, senza esagerare con l'astrazione.","\u002F\u002F integration_test\u002Fsupport\u002Fpump.dart\nextension PumpX on WidgetTester {\n  Future\u003Cvoid> pumpUntilFound(\n    Finder finder, {\n    Duration timeout = const Duration(seconds: 10),\n    Duration step = const Duration(milliseconds: 50),\n  }) async {\n    final deadline = DateTime.now().add(timeout);\n    while (DateTime.now().isBefore(deadline)) {\n      await pump(step);\n      if (finder.evaluate().isNotEmpty) return;\n    }\n    throw TestFailure('Widget non trovato entro $timeout: $finder');\n  }\n}\n\nclass LoginRobot {\n  LoginRobot(this.tester);\n  final WidgetTester tester;\n\n  Future\u003Cvoid> login(String email, String password) async {\n    await tester.enterText(find.byKey(const Key('email')), email);\n    await tester.enterText(find.byKey(const Key('password')), password);\n    await tester.tap(find.byKey(const Key('submit')));\n    await tester.pump();\n  }\n}","Le attese si adattano alla velocità reale del device ed emettono errori diagnostici chiari.",{"id":63,"position":64,"title":65,"body":66,"code_snippet":67,"code_language":39,"expected_result":68,"demo_url":41,"video_url":41},894,5,"Scrivere lo scenario end-to-end completo","Mettiamo tutto insieme: avvio del backend, configurazione dell'app, percorso login → lista → dettaglio, e verifica delle chiamate HTTP effettivamente ricevute.\n\nNota il `tearDown` che chiude il server: senza di esso le porte restano occupate tra i test.","\u002F\u002F integration_test\u002Flogin_flow_test.dart\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  late FakeBackend backend;\n  late TestClock clock;\n\n  setUp(() async {\n    backend = FakeBackend();\n    clock = TestClock();\n    await backend.start();\n    backend.routes['POST \u002Flogin'] = (_) => {'token': 'abc123'};\n    backend.routes['GET \u002Forders'] = (_) => [\n          {'id': 1, 'title': 'Ordine A', 'createdAt': '2024-03-15T08:00:00Z'},\n        ];\n  });\n\n  tearDown(() => backend.stop());\n\n  testWidgets('login e apertura dettaglio ordine', (tester) async {\n    await tester.pumpWidget(buildApp(AppConfig(\n      baseUrl: backend.baseUrl,\n      now: clock.call,\n      animationScale: Duration.zero,\n    )));\n    await tester.pumpAndSettle();\n\n    await LoginRobot(tester).login('mario@test.it', 'segreta');\n    await tester.pumpUntilFound(find.text('Ordine A'));\n\n    expect(find.text('2 ore fa'), findsOneWidget);\n\n    await tester.tap(find.text('Ordine A'));\n    await tester.pumpUntilFound(find.byKey(const Key('order-detail')));\n\n    expect(backend.calls, containsAllInOrder(['POST \u002Flogin', 'GET \u002Forders']));\n  });\n}","Il test passa in modo ripetibile su emulatore e device, senza dipendere dal backend reale né dall'orario.",{"id":70,"position":71,"title":72,"body":73,"code_snippet":74,"code_language":75,"expected_result":76,"demo_url":41,"video_url":41},895,6,"Eseguire la suite in CI senza flakiness residua","Ultimo passo: integrazione in CI. Alcune regole che fanno la differenza:\n\n- Esegui con `flutter test integration_test --dart-define=CI=true`.\n- Niente retry automatici *mascheranti*: un test che fallisce a intermittenza va corretto, non ripetuto.\n- Disabilita le animazioni di sistema sull'emulatore Android.\n- Salva screenshot sui fallimenti con `binding.takeScreenshot`.\n\nAggiungi anche un controllo: se il `FakeBackend` riceve una rotta non registrata (404), il test deve fallire esplicitamente, così nessuna chiamata di rete sfugge al controllo.","# .github\u002Fworkflows\u002Fe2e.yml (estratto)\n- name: Avvia emulatore\n  uses: reactivecircus\u002Fandroid-emulator-runner@v2\n  with:\n    api-level: 34\n    disable-animations: true\n    script: flutter test integration_test --reporter expanded","yaml","Suite E2E verde e stabile in CI, con fallimenti sempre riconducibili a regressioni reali.",[],1793444669192]