[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-patrol-in-flutter-test-di-integrazione-che-toccano-anche-la-ui-nativa":3,"comments-article-patrol-in-flutter-test-di-integrazione-che-toccano-anche-la-ui-nativa":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},121,"Patrol in Flutter: test di integrazione che toccano anche la UI nativa","patrol-in-flutter-test-di-integrazione-che-toccano-anche-la-ui-nativa","I test di integrazione di Flutter si fermano al confine del framework: dialog di permessi, notifiche e impostazioni di sistema restano fuori portata. Patrol colma questa lacuna con finder più espressivi e un'API per automatizzare la UI nativa di Android e iOS.","## Il limite dei test di integrazione \"standard\"\n\nIl pacchetto `integration_test` incluso nell'SDK Flutter è ottimo per verificare flussi completi dell'app: si avvia il widget tree reale, si simulano tap e digitazioni, si controlla il risultato. Il problema arriva nel momento in cui il flusso esce dal framework:\n\n- il dialog di sistema che chiede il permesso per fotocamera, posizione o notifiche;\n- la notifica push che l'utente deve toccare nella shade di Android;\n- il selettore di file o la galleria;\n- il login con WebView di un provider OAuth;\n- lo spegnimento del Wi-Fi per testare la modalità offline.\n\nTutto questo è UI **nativa**, renderizzata fuori dal `FlutterView`: i finder di `flutter_test` non la vedono nemmeno. Il risultato classico è un test che si blocca su un permesso o, peggio, un flusso critico che resta non coperto.\n\n[Patrol](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fpatrol), sviluppato da LeanCode, risolve esattamente questo: estende `integration_test` con un layer di automazione nativa (UIAutomator su Android, XCUITest su iOS) e aggiunge una sintassi di finder molto più compatta.\n\n## Cosa offre Patrol\n\nPatrol si compone di tre pezzi:\n\n1. **`patrol`**: il pacchetto Dart da usare nei test, con l'oggetto `$` (un `PatrolIntegrationTester`) e l'API `$.native`.\n2. **`patrol_finders`**: la libreria di finder, utilizzabile anche da sola nei normali widget test.\n3. **`patrol_cli`**: la CLI che costruisce ed esegue i test sui dispositivi, con tanto di hot restart durante lo sviluppo.\n\n## Installazione\n\nAggiungi la dipendenza di sviluppo e la sezione di configurazione nel `pubspec.yaml`:\n\n```yaml\ndev_dependencies:\n  patrol: ^3.15.0\n\npatrol:\n  app_name: La Mia App\n  android:\n    package_name: com.example.myapp\n  ios:\n    bundle_id: com.example.myapp\n```\n\nPoi installa la CLI e verifica l'ambiente:\n\n```bash\ndart pub global activate patrol_cli\npatrol doctor\n```\n\n### Setup nativo (una tantum)\n\nSu **Android** serve il runner di Patrol in `android\u002Fapp\u002Fbuild.gradle`:\n\n```groovy\nandroid {\n  defaultConfig {\n    testInstrumentationRunner \"pl.leancode.patrol.PatrolJUnitRunner\"\n    testInstrumentationRunnerArguments clearPackageData: \"true\"\n  }\n  testOptions {\n    execution \"ANDROIDX_TEST_ORCHESTRATOR\"\n  }\n}\n\ndependencies {\n  androidTestUtil \"androidx.test:orchestrator:1.5.1\"\n}\n```\n\ne una classe di test in `android\u002Fapp\u002Fsrc\u002FandroidTest\u002Fjava\u002F\u003Cpackage>\u002FMainActivityTest.java` che registra i test Dart nel runner JUnit (il boilerplate è riportato nella documentazione ufficiale e non va modificato).\n\nSu **iOS** occorre aggiungere un target *UI Testing Bundle* chiamato `RunnerUITests` con il file `RunnerUITests.swift` fornito da Patrol, e dichiarare il pod `patrol` nel `Podfile`. È la parte più noiosa, ma si fa una volta sola per progetto.\n\n## Il primo test\n\nI file vanno in `integration_test\u002F`. Al posto di `testWidgets` si usa `patrolTest`, e al posto di `tester` si riceve `$`:\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:patrol\u002Fpatrol.dart';\nimport 'package:myapp\u002Fmain.dart' as app;\n\nvoid main() {\n  patrolTest('login con credenziali valide', ($) async {\n    await $.pumpWidgetAndSettle(const app.MyApp());\n\n    await $(#emailField).enterText('mario@example.com');\n    await $(#passwordField).enterText('super-segreta');\n    await $(FilledButton).tap();\n\n    await $('Benvenuto, Mario').waitUntilVisible();\n  });\n}\n```\n\nDue dettagli importanti:\n\n- `$(...)` è una funzione \"universale\": accetta una `Key`, una stringa (match parziale sul testo), un `Type` di widget, un `IconData` o un `Finder` di `flutter_test`.\n- ogni azione (`tap`, `enterText`) esegue automaticamente un `pumpAndSettle` e attende che il widget sia visibile prima di agire. Addio a sequenze infinite di `await tester.pumpAndSettle()`.\n\n## Finder espressivi\n\nLa vera comodità arriva nella composizione:\n\n```dart\n\u002F\u002F Il bottone dentro la Card che contiene il testo 'Abbonamento Pro'\nawait $(Card).containing('Abbonamento Pro').$(ElevatedButton).tap();\n\n\u002F\u002F Il terzo elemento della lista\nawait $(ListTile).at(2).tap();\n\n\u002F\u002F Scroll automatico fino a trovare l'elemento\nawait $('Impostazioni avanzate').scrollTo().tap();\n\n\u002F\u002F Predicati tipizzati\nexpect($(Text).which\u003CText>((w) => w.data!.startsWith('Totale')), findsOneWidget);\n\n\u002F\u002F Verifiche\nexpect($('Errore di rete').exists, isFalse);\nexpect($(#avatar).visible, isTrue);\n\n\u002F\u002F Attese con timeout personalizzato\nawait $('Pagamento completato')\n    .waitUntilVisible(timeout: const Duration(seconds: 20));\n```\n\n`exists` indica che il widget è nell'albero, `visible` che è effettivamente renderizzato sullo schermo: una distinzione che evita falsi positivi con `Offstage` o liste virtualizzate.\n\n## Automatizzare la UI nativa\n\nQui Patrol fa la differenza. L'oggetto `$.native` espone comandi eseguiti dal driver nativo:\n\n```dart\npatrolTest('scatta una foto concedendo il permesso', ($) async {\n  await $.pumpWidgetAndSettle(const app.MyApp());\n\n  await $(#openCamera).tap();\n\n  if (await $.native.isPermissionDialogVisible()) {\n    await $.native.grantPermissionWhenInUse();\n  }\n\n  await $('Anteprima').waitUntilVisible();\n});\n```\n\nAltri comandi utili:\n\n```dart\n\u002F\u002F Permessi\nawait $.native.grantPermissionOnlyThisTime();\nawait $.native.denyPermission();\nawait $.native.selectFineLocation();\n\n\u002F\u002F Notifiche\nawait $.native.openNotifications();\nawait $.native.tapOnNotificationBySubtitle('Nuovo messaggio');\n\n\u002F\u002F Ciclo di vita e sistema\nawait $.native.pressHome();\nawait $.native.openApp();\nawait $.native.pressBack();\nawait $.native.disableWifi();\nawait $.native.enableWifi();\n\n\u002F\u002F Interazione generica con selettori nativi\nawait $.native.tap(Selector(text: 'Consenti'));\nawait $.native.enterTextByIndex('mario@example.com', index: 0);\n```\n\nUn caso tipico: verificare che l'app gestisca correttamente l'assenza di rete.\n\n```dart\npatrolTest('mostra la banner offline quando manca la rete', ($) async {\n  await $.pumpWidgetAndSettle(const app.MyApp());\n\n  await $.native.disableWifi();\n  await $.native.disableCellular();\n\n  await $(#refreshButton).tap();\n  await $('Sei offline').waitUntilVisible();\n\n  await $.native.enableWifi();\n  await $.native.enableCellular();\n});\n```\n\n### Configurazione globale e setup condiviso\n\n`patrolSetUp` e `patrolTearDown` permettono di eseguire codice asincrono (anche nativo) prima e dopo ogni test, mentre `NativeAutomatorConfig` consente di impostare timeout e nomi dei pulsanti di sistema per lingue diverse dall'inglese:\n\n```dart\nvoid main() {\n  final config = NativeAutomatorConfig(\n    findTimeout: const Duration(seconds: 20),\n    packageName: 'com.example.myapp',\n    bundleId: 'com.example.myapp',\n  );\n\n  patrolSetUp(() async {\n    await SharedPreferences.getInstance().then((p) => p.clear());\n  });\n\n  patrolTest('flusso onboarding', nativeAutomatorConfig: config, ($) async {\n    \u002F\u002F ...\n  });\n}\n```\n\n## Eseguire i test\n\nCon un emulatore o un dispositivo collegato:\n\n```bash\n# Esegue tutti i test in integration_test\u002F\npatrol test\n\n# Un singolo file\npatrol test --target integration_test\u002Flogin_test.dart\n\n# Modalità sviluppo con hot restart del test (premi 'r')\npatrol develop --target integration_test\u002Flogin_test.dart\n```\n\n`patrol develop` è la funzionalità che cambia davvero la produttività: invece di ricostruire l'APK a ogni modifica del test (operazione da minuti), si riavvia il test in pochi secondi.\n\n## Patrol in CI\n\nIn pipeline conviene separare build ed esecuzione, così da poter usare servizi come Firebase Test Lab o device farm commerciali:\n\n```bash\npatrol build android --target integration_test\u002Flogin_test.dart --verbose\n\ngcloud firebase test android run \\\n  --type instrumentation \\\n  --app build\u002Fapp\u002Foutputs\u002Fapk\u002Fdebug\u002Fapp-debug.apk \\\n  --test build\u002Fapp\u002Foutputs\u002Fapk\u002FandroidTest\u002Fdebug\u002Fapp-debug-androidTest.apk \\\n  --device model=MediumPhone.arm,version=33 \\\n  --use-orchestrator \\\n  --timeout 10m\n```\n\nPer iOS la logica è analoga con `patrol build ios` e l'esecuzione dello `.xctestrun` prodotto. Essendo test reali su dispositivo, conviene tenerli in un job separato dalla pipeline di unit\u002Fwidget test, che deve restare veloce.\n\n## Solo i finder, senza automazione nativa\n\nSe la sintassi `$` ti piace ma non ti serve il layer nativo, puoi usare `patrol_finders` nei normali widget test, senza alcun setup di progetto:\n\n```dart\nimport 'package:patrol_finders\u002Fpatrol_finders.dart';\n\nvoid main() {\n  patrolWidgetTest('il contatore incrementa', ($) async {\n    await $.pumpWidgetAndSettle(const MyApp());\n    await $(FloatingActionButton).tap();\n    expect($('1'), findsOneWidget);\n  });\n}\n```\n\nFunziona ovunque giri `flutter test`, web e desktop compresi, ed è un ottimo modo per adottare Patrol in modo graduale.\n\n## Best practice e avvertenze\n\n- **Usa le `Key`**: `$(#loginButton)` è infinitamente più stabile di `$('Accedi')`, che si rompe al primo cambio di copy o con l'app tradotta.\n- **Non trasformare Patrol nella tua suite principale**: un test di integrazione costa decine di secondi, un widget test millisecondi. Copri con Patrol i 5-10 flussi critici (onboarding, login, acquisto, permessi), il resto a livelli inferiori.\n- **Attenzione alla lingua del dispositivo**: i pulsanti dei dialog di sistema cambiano testo. Fissa la locale dell'emulatore in CI o usa i metodi dedicati (`grantPermissionWhenInUse`) invece di `Selector(text: ...)`.\n- **Isola lo stato**: `clearPackageData: \"true\"` su Android e un `patrolSetUp` che resetta preferenze e database evitano test dipendenti dall'ordine di esecuzione.\n- **Mock del backend**: puntare a un ambiente di staging reale rende i test fragili. Un server locale o un'istanza dedicata con dati seminati è la scelta più solida.\n- **L'automazione nativa copre Android e iOS**. Per web e desktop restano i finder più `integration_test` classico.\n\n## Conclusioni\n\nPatrol non sostituisce `integration_test`: lo potenzia. Con un setup iniziale di mezz'ora ottieni finder molto più leggibili, attese implicite che eliminano gran parte della flakiness e, soprattutto, la possibilità di testare i punti in cui la tua app incontra il sistema operativo — proprio quelli dove gli utenti incontrano più bug. Se hai flussi che oggi verifichi a mano prima di ogni release, è lì che conviene iniziare.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F3cc69675-3ea8-4109-98f5-e10830bbaab6.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1618927376764-c8482e339321?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE0MzIxMDl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Árpád Czapp","https:\u002F\u002Funsplash.com\u002F@czapp_arpad","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fperson-holding-white-and-blue-card-3XVSkqlJI0k",null,"published","2026-10-08T04:01:49+00:00","Patrol: test di integrazione nativi in Flutter","Guida pratica a Patrol in Flutter: finder espressivi, automazione della UI nativa (permessi, notifiche, Wi-Fi), hot restart dei test e integrazione in CI.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316906680]