dart-generate-test-mocks

Par flutter · skills

Définissez et générez des objets mock pour les dépendances externes avec `package:mockito` et `build_runner`. À utiliser lors des tests unitaires de classes qui dépendent de services externes complexes tels que des API ou des bases de données.

npx skills add https://github.com/flutter/skills --skill dart-generate-test-mocks

Test et simulation des applications Dart

Sommaire

Structurer le code pour la testabilité

Concevez les classes Dart pour supporter l'injection de dépendances. Isolez les dépendances externes complexes (comme les clients API ou les bases de données) afin qu'elles puissent être remplacées par des objets mock lors des tests.

  • Injectez les services externes (par ex., http.Client) via les constructeurs de classe.
  • Représentez les URLs strictement comme des objets Uri en utilisant Uri.parse(string).
  • Utilisez les fonctionnalités orientées objet de Dart (classes, mixins) pour définir des interfaces claires pour les interactions externes.

Gérer les dépendances

Configurez le fichier pubspec.yaml avec les paquets nécessaires de test et de génération de code.

  • Ajoutez les dépendances runtime (par ex., package:http) en utilisant dart pub add http.
  • Ajoutez les dépendances de test en utilisant dart pub add dev:test dev:mockito dev:build_runner.
  • Importez les bibliothèques HTTP avec un préfixe pour éviter les collisions d'espace de noms : import 'package:http/http.dart' as http;.

Générer des mocks

Utilisez package:mockito et build_runner pour générer automatiquement des classes mock pour des scénarios fixes et la vérification du comportement.

  • Utilisez toujours l'annotation @GenerateNiceMocks (préférable à @GenerateMocks pour éviter les exceptions de stub manquant).
  • Placez l'annotation dans le fichier de test en passant une liste d'objets MockSpec<Type>().
  • Importez le fichier généré en utilisant l'extension .mocks.dart.
  • Exécutez build_runner pour générer les fichiers mock : dart run build_runner build.

Implémenter les tests unitaires

Isolez le système en test en utilisant les objets mock générés. Utilisez package:test pour structurer la suite de test.

  • Stubbing : Configurez le comportement du mock avant d'interagir avec le système en test.
    • Utilisez when(mock.method()).thenReturn(value) pour les méthodes synchrones.
    • CRITIQUE : Utilisez toujours thenAnswer((_) async => value) pour les méthodes retournant un Future ou un Stream. Ne utilisez jamais thenReturn pour les retours asynchrones.
  • Vérification : Vérifiez que le système en test a interagi correctement avec l'objet mock.
    • Utilisez verify(mock.method()).called(1) pour vérifier les compteurs d'invocation exacts.
    • Utilisez des argument matchers comme any, anyNamed, ou captureAny pour une vérification flexible.

Flux de travail : créer et exécuter des tests simulés

Utilisez la liste de contrôle suivante pour implémenter et vérifier des tests unitaires simulés.

Progression des tâches

  • [ ] 1. Identifiez la dépendance externe à simuler (par ex., http.Client).
  • [ ] 2. Injectez la dépendance dans le constructeur de la classe cible.
  • [ ] 3. Créez un fichier de test (par ex., target_test.dart) et ajoutez @GenerateNiceMocks([MockSpec<Dependency>()]).
  • [ ] 4. Ajoutez la directive part ou import pour le fichier .mocks.dart généré.
  • [ ] 5. Exécutez dart run build_runner build pour générer les classes mock.
  • [ ] 6. Écrivez les cas de test en utilisant group() et test().
  • [ ] 7. Stubifiez les comportements requis en utilisant when().
  • [ ] 8. Exécutez la méthode cible.
  • [ ] 9. Vérifiez les interactions en utilisant verify() et vérifiez les résultats en utilisant expect().
  • [ ] 10. Exécutez la suite de test en utilisant dart test.

Boucle de rétroaction : échecs de tests

Si les tests échouent ou que build_runner rencontre des erreurs :

  1. Exécutez le validateur : Exécutez dart test ou dart run build_runner build.
  2. Examinez les erreurs : Vérifiez les stubs manquants, les argument matchers incompatibles, ou les erreurs de syntaxe dans les fichiers générés.
  3. Corrigez :
    • Si une méthode mock lève une erreur null inattendue, assurez-vous que vous avez utilisé @GenerateNiceMocks.
    • Si un stub asynchrone lève un ArgumentError, changez thenReturn en thenAnswer.
    • Si build_runner échoue, assurez-vous que l'import .mocks.dart correspond exactement au nom du fichier.
  4. Répétez jusqu'à ce que tous les tests réussissent.

Exemples

Exemple de test et simulation haute fidélité

1. Système en test (lib/api_service.dart)

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiService {
  final http.Client client;

  ApiService(this.client);

  Future<String> fetchData(String urlString) async {
    final uri = Uri.parse(urlString);
    final response = await client.get(uri);

    if (response.statusCode == 200) {
      return jsonDecode(response.body)['data'];
    } else {
      throw Exception('Failed to load data');
    }
  }
}

2. Implémentation de test (test/api_service_test.dart)

import 'package:test/test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
import 'package:my_app/api_service.dart';

// Generate the mock class for http.Client
@GenerateNiceMocks([MockSpec<http.Client>()])
import 'api_service_test.mocks.dart';

void main() {
  group('ApiService', () {
    late ApiService apiService;
    late MockClient mockHttpClient;

    setUp(() {
      mockHttpClient = MockClient();
      apiService = ApiService(mockHttpClient);
    });

    test('returns data if the http call completes successfully', () async {
      // Arrange: Stub the async HTTP GET request using thenAnswer
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('{"data": "Success"}', 200),
      );

      // Act
      final result = await apiService.fetchData('https://api.example.com/data');

      // Assert
      expect(result, 'Success');

      // Verify the mock was called with the correct Uri
      verify(mockHttpClient.get(Uri.parse('https://api.example.com/data'))).called(1);
    });

    test('throws an exception if the http call completes with an error', () {
      // Arrange
      when(mockHttpClient.get(any)).thenAnswer(
        (_) async => http.Response('Not Found', 404),
      );

      // Act & Assert
      expect(
        apiService.fetchData('https://api.example.com/data'),
        throwsException,
      );
    });
  });
}

Skills similaires