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.
  • pump ricostruisce un singolo frame, mentre pumpAndSettle attende 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 setUp e tearDown per isolare lo stato.
  • Assegna delle Key ai widget interattivi per renderli facilmente individuabili.
  • Misura la copertura con flutter test --coverage e analizza il file lcov.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.