[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-golden-test-in-flutter-prevenire-le-regressioni-visive-della-ui":3,"comments-article-golden-test-in-flutter-prevenire-le-regressioni-visive-della-ui":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},78,"Golden test in Flutter: prevenire le regressioni visive della UI","golden-test-in-flutter-prevenire-le-regressioni-visive-della-ui","I golden test confrontano screenshot dei widget con immagini di riferimento per intercettare le regressioni grafiche. Guida pratica: setup, font reali, più dimensioni di schermo, tolleranza e CI.","## Perché i golden test\n\nUnit test e widget test verificano la logica e la presenza dei widget nell'albero, ma non dicono nulla su **come appare** davvero la UI. Un padding cambiato per sbaglio, un `ThemeExtension` modificato, un aggiornamento di Flutter che altera il rendering del testo: sono tutte regressioni che passano indenni attraverso una suite di test tradizionale.\n\nI **golden test** (o snapshot test) risolvono esattamente questo problema: renderizzano un widget, ne catturano un'immagine PNG e la confrontano byte per byte con un file di riferimento (il *golden*) versionato nel repository. Se l'immagine cambia, il test fallisce e la differenza viene salvata su disco per l'ispezione visiva.\n\nSono particolarmente utili per:\n\n- design system e librerie di componenti condivisi;\n- schermate con layout complessi e stati multipli (loading, errore, lista vuota);\n- verifica di temi chiaro\u002Fscuro e di più lingue;\n- controllo delle rotture dopo un upgrade di Flutter o di un package UI.\n\n## Il primo golden test\n\nNon serve alcuna dipendenza esterna: il matcher `matchesGoldenFile` fa parte di `flutter_test`.\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:my_app\u002Fui\u002Fprice_badge.dart';\n\nvoid main() {\n  testWidgets('PriceBadge - stato scontato', (tester) async {\n    await tester.pumpWidget(\n      MaterialApp(\n        theme: ThemeData(useMaterial3: true),\n        home: const Scaffold(\n          body: Center(\n            child: PriceBadge(price: 49.90, discount: 0.30),\n          ),\n        ),\n      ),\n    );\n\n    await expectLater(\n      find.byType(PriceBadge),\n      matchesGoldenFile('goldens\u002Fprice_badge_discount.png'),\n    );\n  });\n}\n```\n\nAl primo avvio il file non esiste e il test fallisce. Si generano (o rigenerano) i golden con:\n\n```bash\nflutter test --update-goldens\n```\n\nIl PNG prodotto va **committato** insieme al codice: è a tutti gli effetti parte della definizione del comportamento atteso. Nelle pull request diventa anche un ottimo strumento di review, perché il diff mostra visivamente come cambia la UI.\n\n> Suggerimento: passa a `matchesGoldenFile` un `Finder` specifico (non l'intero `MaterialApp`) per ottenere immagini piccole, mirate e meno fragili.\n\n## Caricare i font reali\n\nProblema classico: il golden generato mostra il testo come una serie di rettangoli neri. È il comportamento voluto di `flutter test`, che sostituisce i font non caricati con il font di test *Ahem*. Per avere screenshot realistici bisogna caricare i font dichiarati nel `pubspec.yaml` prima dell'esecuzione dei test.\n\nIl punto giusto in cui farlo è il file speciale `test\u002Fflutter_test_config.dart`, che Flutter esegue automaticamente prima di ogni file di test della cartella.\n\n```dart\n\u002F\u002F test\u002Fflutter_test_config.dart\nimport 'dart:async';\nimport 'dart:convert';\n\nimport 'package:flutter\u002Fservices.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nFuture\u003Cvoid> testExecutable(FutureOr\u003Cvoid> Function() testMain) async {\n  TestWidgetsFlutterBinding.ensureInitialized();\n  await _loadAppFonts();\n  await testMain();\n}\n\nFuture\u003Cvoid> _loadAppFonts() async {\n  final manifest = await rootBundle.loadStructuredData\u003CList\u003Cdynamic>>(\n    'FontManifest.json',\n    (data) async => json.decode(data) as List\u003Cdynamic>,\n  );\n\n  for (final family in manifest) {\n    final loader = FontLoader(family['family'] as String);\n    for (final font in family['fonts'] as List\u003Cdynamic>) {\n      loader.addFont(rootBundle.load(font['asset'] as String));\n    }\n    await loader.load();\n  }\n}\n```\n\nAttenzione: `FontManifest.json` contiene solo i font dichiarati come asset. Se usi `google_fonts`, in ambiente di test il package tenta il download via rete (bloccato) e ricade sul font di sistema: la soluzione robusta è includere i file `.ttf` tra gli asset dell'app e disabilitare il fetch runtime.\n\n## Testare più dimensioni di schermo\n\nUn componente può rompersi solo su schermi stretti o con testo ingrandito. Con `tester.view` puoi controllare dimensioni e densità di pixel, ricordandoti sempre di ripristinare lo stato.\n\n```dart\nFuture\u003Cvoid> pumpAt(\n  WidgetTester tester,\n  Widget child, {\n  Size size = const Size(390, 844),\n  double textScale = 1.0,\n}) async {\n  tester.view.physicalSize = size * tester.view.devicePixelRatio;\n  tester.view.devicePixelRatio = 3.0;\n  addTearDown(tester.view.reset);\n\n  await tester.pumpWidget(\n    MediaQuery(\n      data: MediaQueryData(textScaler: TextScaler.linear(textScale)),\n      child: MaterialApp(home: child),\n    ),\n  );\n}\n```\n\nE poi si generano più varianti in modo tabellare:\n\n```dart\nvoid main() {\n  const devices = \u003CString, Size>{\n    'phone': Size(390, 844),\n    'tablet': Size(834, 1112),\n  };\n\n  for (final entry in devices.entries) {\n    testWidgets('ProductCard - ${entry.key}', (tester) async {\n      await pumpAt(tester, const ProductCard(), size: entry.value);\n      await expectLater(\n        find.byType(ProductCard),\n        matchesGoldenFile('goldens\u002Fproduct_card_${entry.key}.png'),\n      );\n    });\n  }\n}\n```\n\nLo stesso pattern funziona benissimo per il ciclo su `Brightness.light` \u002F `Brightness.dark` e su `textScale` 1.0 \u002F 1.5 (test di accessibilità visiva).\n\n## Attese, immagini e stati asincroni\n\nUn golden catturato mentre è in corso un'animazione è la ricetta perfetta per un test *flaky*. Alcune regole:\n\n- usa `await tester.pumpAndSettle()` per far concludere le animazioni, oppure `await tester.pump(const Duration(milliseconds: 300))` se vuoi fotografare un frame preciso;\n- non caricare immagini di rete: sostituiscile con un `NetworkImage` fittizio o inietta un `ImageProvider` di test (`MemoryImage` con un PNG minimale);\n- rendi deterministico tutto ciò che dipende dal tempo o dal caso: date, id generati, shuffle di liste.\n\n## Tolleranza: convivere con differenze di un pixel\n\nIl confronto di default è esatto: basta un pixel diverso per far fallire il test. Su motori di rendering leggermente diversi (o dopo un aggiornamento del framework) questo diventa ingestibile. Si può registrare un comparator con una soglia di tolleranza.\n\n```dart\n\u002F\u002F test\u002Fflutter_test_config.dart (estratto)\nclass TolerantComparator extends LocalFileComparator {\n  TolerantComparator(super.testFile, {this.threshold = 0.005});\n\n  final double threshold;\n\n  @override\n  Future\u003Cbool> compare(Uint8List imageBytes, Uri golden) async {\n    final result = await GoldenFileComparator.compareLists(\n      imageBytes,\n      await getGoldenBytes(golden),\n    );\n    if (result.passed || result.diffPercent \u003C= threshold) return true;\n\n    final error = await generateFailureOutput(result, golden, basedir);\n    throw FlutterError(error);\n  }\n}\n\nvoid _installComparator() {\n  final current = goldenFileComparator as LocalFileComparator;\n  goldenFileComparator = TolerantComparator(\n    Uri.parse('${current.basedir}test.dart'),\n    threshold: 0.005, \u002F\u002F 0,5% di pixel differenti\n  );\n}\n```\n\nUsa soglie basse (0,1–0,5%): una tolleranza troppo generosa rende i test inutili.\n\n## Golden test in CI\n\nIl rendering di Flutter **non è identico su tutte le piattaforme**: font di sistema, antialiasing e sottosistemi grafici producono immagini leggermente diverse tra macOS, Windows e Linux. Se generi i golden sul Mac di uno sviluppatore e li verifichi su un runner Linux, avrai fallimenti sistematici.\n\nStrategie consolidate:\n\n1. **Una sola piattaforma di riferimento.** Genera e verifica i golden sempre nello stesso ambiente (tipicamente un container Linux con versione di Flutter fissata).\n2. **Taggare i test.** Metti `@Tags(['golden'])` in cima ai file di golden test e nel `dart_test.yaml` escludili dalla run standard, eseguendoli in un job dedicato con `flutter test --tags golden`.\n3. **Pubblicare i diff.** In caso di fallimento Flutter scrive le immagini in `test\u002Ffailures\u002F`: caricale come artifact della pipeline per poterle ispezionare.\n\nEsempio di step in GitHub Actions:\n\n```yaml\n- name: Golden tests\n  run: flutter test --tags golden\n\n- name: Upload golden failures\n  if: failure()\n  uses: actions\u002Fupload-artifact@v4\n  with:\n    name: golden-failures\n    path: '**\u002Ftest\u002Ffailures\u002F**'\n```\n\n## Package di supporto\n\nStoricamente il riferimento era `golden_toolkit`, oggi non più mantenuto attivamente. Le alternative attuali sono:\n\n- **alchemist**: pensato per la CI, distingue tra golden \"CI\" (senza font di sistema, deterministici) e golden \"platform\", e offre le *golden scene* con più stati in una sola immagine;\n- **golden_screenshot** o setup custom come quello mostrato sopra, spesso sufficiente per progetti medi.\n\nIn ogni caso il consiglio è di incapsulare la logica di pump in una funzione helper del progetto: cambiare libreria domani costerà una sola modifica.\n\n## Best practice riassunte\n\n- Un golden per **componente e stato**, non per intere schermate popolate di dati reali.\n- Nomi di file espliciti e organizzati in cartelle (`test\u002Fgoldens\u002F\u003Cfeature>\u002F...`).\n- Rigenera i golden solo con un `--update-goldens` intenzionale, mai \"per far passare la CI\".\n- Nella code review guarda sempre l'immagine, non solo il fatto che il test sia verde.\n- Mantieni fissata la versione di Flutter nella pipeline: è la variabile che più spesso invalida i golden.\n\n## Conclusioni\n\nI golden test sono il livello di test che manca a molte app Flutter: costano poco da scrivere, si integrano con la suite esistente e offrono una rete di sicurezza sull'aspetto della UI, la parte del prodotto che gli utenti giudicano per prima. Parti dai componenti del tuo design system, aggiungi le varianti tema\u002Fdimensione\u002Fscala del testo e collega tutto a un job CI dedicato: in poche ore avrai una difesa concreta contro le regressioni visive.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F68b6f6ac-a381-4f27-9e42-4f7940380346.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1559650956-f5ce6186cc4d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODc3MTY4ODd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Brando Makes Branding","https:\u002F\u002Funsplash.com\u002F@brandomakesbranding","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fturned-on-silver-imac-on-table-B0kRAfteJZY",null,"published","2026-08-26T04:01:28+00:00","Golden test in Flutter: guida alle regressioni visive","Come scrivere golden test in Flutter: matchesGoldenFile, font reali, più dimensioni schermo, tolleranza dei pixel e integrazione in CI senza test flaky.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120585573]