Test unitari in Flutter con mockito e mocktail: mockare le dipendenze
Best practiceIntermedio35 min Flutter 3.x

Test unitari in Flutter con mockito e mocktail: mockare le dipendenze

Test unitari con i mock in Flutter

Quando testiamo la logica di business (repository, service, controller), non vogliamo dipendere da chiamate HTTP reali o da database. La soluzione è mockare le dipendenze: sostituirle con oggetti finti che restituiscono valori controllati.

In questo tutorial vedremo come scrivere test unitari isolati usando due librerie molto diffuse:

  • mockito: la libreria storica, basata su generazione di codice con build_runner.
  • mocktail: alternativa moderna, senza generazione di codice, con una sintassi più snella.

Alla fine avrai un UserRepository testato al 100% senza mai toccare la rete.

  1. 1

    Aggiungere le dipendenze di test

    Aggiungiamo le librerie necessarie nella sezione dev_dependencies del pubspec.yaml. Includiamo sia mockito (che richiede build_runner) sia mocktail, così potrai confrontare i due approcci. Il pacchetto http ci serve come dipendenza da mockare.

    dependencies:
      http: ^1.2.0
    
    dev_dependencies:
      flutter_test:
        sdk: flutter
      mockito: ^5.4.4
      build_runner: ^2.4.9
      mocktail: ^1.0.3

    Risultato atteso

    Dopo aver salvato, esegui `flutter pub get` senza errori.

  2. 2

    Creare la classe da testare

    Definiamo un semplice UserRepository che dipende da un http.Client. Iniettare la dipendenza dal costruttore è fondamentale: ci permette di passare un client finto durante i test.

    import 'dart:convert';
    import 'package:http/http.dart' as http;
    
    class User {
      final int id;
      final String name;
      User({required this.id, required this.name});
    
      factory User.fromJson(Map<String, dynamic> json) =>
          User(id: json['id'] as int, name: json['name'] as String);
    }
    
    class UserRepository {
      final http.Client client;
      UserRepository(this.client);
    
      Future<User> fetchUser(int id) async {
        final response = await client.get(
          Uri.parse('https://api.example.com/users/$id'),
        );
        if (response.statusCode == 200) {
          return User.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
        }
        throw Exception('Errore nel caricamento utente');
      }
    }

    Risultato atteso

    Il file `lib/user_repository.dart` compila correttamente.

  3. 3

    Generare i mock con mockito

    Con mockito dichiariamo quali classi mockare tramite l'annotazione @GenerateMocks, poi eseguiamo build_runner per generare il codice. Crea il file test/user_repository_test.dart.

    Esegui in terminale:

    flutter pub run build_runner build --delete-conflicting-outputs
    

    Questo genera il file user_repository_test.mocks.dart con la classe MockClient.

    import 'package:flutter_test/flutter_test.dart';
    import 'package:http/http.dart' as http;
    import 'package:mockito/annotations.dart';
    
    import 'user_repository_test.mocks.dart';
    
    @GenerateMocks([http.Client])
    void main() {
      // I test verranno aggiunti nel prossimo passo
    }

    Risultato atteso

    Viene creato il file `user_repository_test.mocks.dart` senza errori di build.

  4. 4

    Scrivere i test con mockito

    Ora usiamo when(...).thenAnswer(...) per definire il comportamento del mock e verify(...) per controllare che il metodo sia stato chiamato. Testiamo sia il caso di successo sia il caso di errore.

    import 'package:flutter_test/flutter_test.dart';
    import 'package:http/http.dart' as http;
    import 'package:mockito/annotations.dart';
    import 'package:mockito/mockito.dart';
    
    import '../lib/user_repository.dart';
    import 'user_repository_test.mocks.dart';
    
    @GenerateMocks([http.Client])
    void main() {
      late MockClient client;
      late UserRepository repository;
    
      setUp(() {
        client = MockClient();
        repository = UserRepository(client);
      });
    
      test('restituisce un User quando la risposta è 200', () async {
        when(client.get(any)).thenAnswer(
          (_) async => http.Response('{"id": 1, "name": "Mario"}', 200),
        );
    
        final user = await repository.fetchUser(1);
    
        expect(user.name, 'Mario');
        verify(client.get(any)).called(1);
      });
    
      test('lancia un\'eccezione quando la risposta non è 200', () async {
        when(client.get(any))
            .thenAnswer((_) async => http.Response('Not found', 404));
    
        expect(() => repository.fetchUser(1), throwsException);
      });
    }

    Risultato atteso

    Eseguendo `flutter test` entrambi i test passano (verde).

  5. 5

    L'alternativa senza codegen: mocktail

    Con mocktail non serve build_runner. Basta creare una classe che estende Mock e implementa l'interfaccia. Un aspetto importante: mocktail richiede di registrare i fallback value per i tipi custom usati con any(), tramite registerFallbackValue. Nota anche l'uso di any() come funzione anziché any.

    import 'package:flutter_test/flutter_test.dart';
    import 'package:http/http.dart' as http;
    import 'package:mocktail/mocktail.dart';
    
    import '../lib/user_repository.dart';
    
    class MockClient extends Mock implements http.Client {}
    
    class FakeUri extends Fake implements Uri {}
    
    void main() {
      late MockClient client;
      late UserRepository repository;
    
      setUpAll(() {
        registerFallbackValue(FakeUri());
      });
    
      setUp(() {
        client = MockClient();
        repository = UserRepository(client);
      });
    
      test('restituisce un User quando la risposta è 200', () async {
        when(() => client.get(any())).thenAnswer(
          (_) async => http.Response('{"id": 2, "name": "Luigi"}', 200),
        );
    
        final user = await repository.fetchUser(2);
    
        expect(user.name, 'Luigi');
        verify(() => client.get(any())).called(1);
      });
    }

    Risultato atteso

    Il test con mocktail passa senza aver eseguito alcuna generazione di codice.

  6. 6

    Verificare argomenti e ordine delle chiamate

    I mock non servono solo a restituire valori finti: permettono di verificare le interazioni. Possiamo controllare gli argomenti esatti con captureAny/captured, l'ordine con verifyInOrder, e assicurarci che non ci siano chiamate impreviste con verifyNoMoreInteractions. Questo esempio usa mocktail.

    test('chiama l\'URL corretto con l\'id dell\'utente', () async {
      when(() => client.get(any())).thenAnswer(
        (_) async => http.Response('{"id": 5, "name": "Anna"}', 200),
      );
    
      await repository.fetchUser(5);
    
      final captured = verify(() => client.get(captureAny())).captured;
      final uri = captured.single as Uri;
    
      expect(uri.path, '/users/5');
      verifyNoMoreInteractions(client);
    });

    Risultato atteso

    Il test conferma che il repository costruisce l'URL corretto e non effettua chiamate extra.

  7. 7

    Best practice e quando usare cosa

    Alcune regole per test manutenibili:

    • Inietta sempre le dipendenze dal costruttore: senza dependency injection non puoi mockare.
    • Un mock per test: usa setUp per ricreare i mock, evitando stato condiviso tra test.
    • Testa i comportamenti, non l'implementazione: verifica input/output, evita di verificare ogni singola chiamata interna se non necessario.
    • mockito vs mocktail:
      • Scegli mocktail per progetti nuovi: niente build_runner, sintassi più pulita, supporto per null safety immediato.
      • Scegli mockito se il progetto lo usa già o se preferisci mock generati e type-safe.

    Esegui l'intera suite con:

    flutter test --coverage
    

    e genera un report di copertura con genhtml coverage/lcov.info -o coverage/html.

    Risultato atteso

    Hai una suite di test unitari isolata, veloce e ripetibile, con la copertura del tuo repository misurabile.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!