Perché testare un'app Flutter
Scrivere test è una delle pratiche più sottovalutate nello sviluppo mobile, eppure è ciò che distingue un progetto fragile da uno robusto. Flutter offre un ecosistema di testing maturo e ben integrato, suddiviso in tre livelli principali:
- Unit test: verificano la logica di una singola funzione, metodo o classe.
- Widget test: validano il comportamento e l'aspetto di un singolo widget.
- Integration test: testano l'app completa o flussi significativi su un dispositivo reale o emulatore.
In questa guida vediamo come implementarli tutti e tre con esempi concreti.
Configurazione iniziale
Il pacchetto flutter_test è già incluso nel SDK. Per gli integration test aggiungi le dipendenze nel pubspec.yaml:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
mockito: ^5.4.4
build_runner: ^2.4.9
La convenzione vuole che i file di test risiedano nella cartella test/ e terminino con il suffisso _test.dart.
Unit test
Gli unit test sono i più veloci da eseguire e dovrebbero costituire la maggior parte della tua suite. Supponiamo di avere una semplice classe Counter:
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
Il test corrispondente:
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter.dart';
void main() {
group('Counter', () {
test('il valore parte da zero', () {
expect(Counter().value, 0);
});
test('increment aumenta il valore', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
});
}
Usa group per organizzare test correlati e expect con i matcher (equals, isNull, throwsA, ecc.) per le asserzioni.
Mocking delle dipendenze
Quando una classe dipende da servizi esterni (API, database), conviene simularne il comportamento con mockito. Definisci le annotazioni e genera i mock:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
@GenerateMocks([ApiService])
void main() {
late MockApiService mockApi;
setUp(() {
mockApi = MockApiService();
});
test('restituisce i dati dell\'utente', () async {
when(mockApi.fetchUser(1))
.thenAnswer((_) async => User(id: 1, name: 'Mario'));
final user = await mockApi.fetchUser(1);
expect(user.name, 'Mario');
verify(mockApi.fetchUser(1)).called(1);
});
}
Genera i file mock con: dart run build_runner build.
Widget test
I widget test eseguono i componenti in un ambiente headless, senza un dispositivo reale. Il cuore è il WidgetTester:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_page.dart';
void main() {
testWidgets('il contatore si incrementa al tap', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Alcuni metodi chiave:
pumpWidget: monta il widget nell'albero di test.find: localizza i widget tramite testo, tipo, chiave o icona.tester.tap/tester.enterText: simulano l'interazione utente.pumpricostruisce un singolo frame, mentrepumpAndSettleattende il completamento di tutte le animazioni.
Integration test
Gli integration test verificano l'app reale in esecuzione. Crea un file nella cartella integration_test/:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('flusso completo di login', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email')), 'test@mail.it');
await tester.enterText(find.byKey(const Key('password')), 'segreta');
await tester.tap(find.byKey(const Key('loginBtn')));
await tester.pumpAndSettle();
expect(find.text('Benvenuto'), findsOneWidget);
});
}
Esegui il test su un dispositivo collegato con:
flutter test integration_test/app_test.dart
Buone pratiche
- Piramide dei test: molti unit test, un numero moderato di widget test, pochi integration test (lenti e costosi).
- Mantieni i test indipendenti: usa
setUpetearDownper isolare lo stato. - Assegna delle Key ai widget interattivi per renderli facilmente individuabili.
- Misura la copertura con
flutter test --coveragee analizza il filelcov.info. - Integra i test nella tua pipeline CI/CD per evitare regressioni.
Conclusione
Una buona suite di test ti permette di rifattorizzare con sicurezza, individuare bug prima della produzione e documentare il comportamento atteso del codice. Inizia con piccoli unit test e amplia gradualmente la copertura: il tempo investito viene ripagato a ogni nuova release.
