Test de widgets avec Flutter: Mise en route

Online Coding Courses for Kids

Les tests sont importants lors du développement de votre application. À mesure que votre produit se développe, il devient plus complexe et effectuer des tests manuels devient plus difficile. Un environnement de test automatisé permet d’optimiser ce processus.

Les tests de widgets sont comme les tests d’interface utilisateur: vous développez l’apparence de votre application, garantissant que chaque interaction effectuée par l’utilisateur produit le résultat attendu.

Pour ce didacticiel, vous allez écrire des tests de widget pour une application de voiture appelée Conduis moi, qui permet aux utilisateurs d’afficher une liste de voitures, d’afficher des détails à leur sujet et de sélectionner et afficher des voitures spécifiques. Au cours de ce processus, vous apprendrez à tester que l’application exécute correctement les fonctions suivantes:

  • Charge des données simulées dans les tests de widget.
  • Injecte des données simulées erronées pour les tests négatifs.
  • Garantit que l’application présente une liste de voitures triées et affiche ses détails.
  • Vérifie que la sélection de voiture apparaît correctement dans la page de liste.
  • Garantit que la page des détails de la voiture s’affiche correctement.

Commencer

Pour commencer, téléchargez le projet de démarrage en cliquant sur le Télécharger les documents en haut ou en bas du didacticiel, puis explorez le projet de démarrage dans Visual Studio Code. Vous pouvez également utiliser Android Studio, mais ce didacticiel utilise le code Visual Studio dans ses exemples.

Assurez-vous d’exécuter flutter packages get soit sur la ligne de commande ou lorsque vous y êtes invité par votre IDE. Cela extrait la dernière version des packages, rxdart et get_it, que ce projet utilise.

Générez et exécutez le projet avec flutter run pour vous familiariser avec le fonctionnement de l’application.

Liste des voitures

Explorer le projet de démarrage

Le projet de démarrage comprend la mise en œuvre de l’application afin que vous puissiez vous concentrer sur les tests de widgets. Jetez un œil au contenu de lib pour comprendre le fonctionnement de l’application.

Structure du projet

Le projet comprend quatre dossiers principaux:

  • base de données
  • détails
  • liste
  • des modèles

base de données contient un fichier principal appelé cars_database.dart, qui implémente une classe abstraite appelée CarsDataProvider qui met en œuvre loadCars(). Cette méthode analyse le fichier JSON, qui contient une liste de données de voiture, et renvoie ces données en tant que CarsList. Le modèle contient alors une liste de voitures et un message d’erreur si une exception se produit.

Ce projet utilise BLoC pour passer des données entre la couche de widgets et la couche de données.

Dans le détails dossier, vous trouverez car_details_bloc.dart, qui obtient des données de CarsListBloc. Il transmet les données au widget CarDetails dans car_details_page.dart.

Ensuite, ouvrez car_details_page.dart. Sur init, il récupère les données via CarDetailsBloc et le présente sur le widget. Lorsque les utilisateurs sélectionnent ou désélectionnent des éléments, CarsListBloc fait les mises à jour.

Ensuite, regardez cars_list_bloc.dart dans le liste dossier. Ceci est la couche de données de CarsList. Cette classe charge les données de JSON et les transmet à la liste des widgets. Par la suite, il trie les voitures par ordre alphabétique via alphabetiseItemsByTitleIgnoreCases().

Lorsque l’utilisateur sélectionne une voiture, un flux de données distinct la gère.

dans le des modèles dossier, vous trouverez un autre fichier important. car.dart est où les implémentations de Car et CarsList résider.

constants.dart contient la plupart des chaînes que vous utiliserez dans l’application. dependency_injector.dart est l’endroit où l’application enregistre les principales classes de couches de données et les injecte via get_it.

Maintenant que vous avez essayé l’application et que vous comprenez les détails de la mise en œuvre, il est temps de commencer à exécuter des tests.

Avant de vous plonger dans le sujet des tests de widgets avec Flutter, reculez et comparez-les avec les tests unitaires.

Tests unitaires vs tests de widgets

Test unitaire vs test de widget

Les tests unitaires sont un processus dans lequel vous vérifiez la qualité, les performances ou la fiabilité en écrivant du code supplémentaire qui garantit que la logique de votre application fonctionne comme prévu. Il teste la logique écrite dans les fonctions et les méthodes. Les tests se développent et s’accumulent ensuite pour couvrir une classe entière et, par la suite, une grande partie du projet, sinon la totalité.

Le but d’un test de widget est de vérifier que l’interface utilisateur de chaque widget ressemble et se comporte comme prévu. Fondamentalement, vous effectuez des tests en restituant les widgets dans le code avec des données factices.

Cela vous indique également que si vous modifiez la logique de l’application – par exemple, vous changez la validation de connexion du nom d’utilisateur d’un minimum de six caractères à sept – alors votre test unitaire et votre test de widget peuvent échouer ensemble.

Les tests verrouillent les fonctionnalités de votre application, ce qui vous aide à planifier correctement la conception de l’application avant de la développer.

Test de la pyramide

Il existe trois types de tests que vous pouvez effectuer avec Flutter:

  • Tests unitaires: Utilisé pour tester une méthode ou une classe.
  • Tests de widgets: Ceux-ci testent un seul widget.
  • Tests d’intégration: Utilisez-les pour tester les flux critiques de l’ensemble de l’application.

Alors, combien de tests aurez-vous besoin? Pour décider, jetez un œil à la pyramide des tests. Il résume les types de tests essentiels qu’une application Flutter devrait avoir:

Test de la pyramide

Essentiellement, les tests unitaires devraient couvrir la majeure partie de l’application, puis les tests de widgets et, enfin, les tests d’intégration.

Même lorsque de bons terrains d’essai sont en place, vous ne devez pas omettre les tests manuels.

Au fur et à mesure que vous montez dans la pyramide, les tests deviennent moins isolés et plus intégrés. Écrire de bons tests unitaires vous aide à construire une base solide pour votre application.

Maintenant que vous comprenez la nécessité des tests, il est temps de plonger dans le projet pour ce tutoriel!

Widget testant la liste des voitures

Avant de commencer les tests de widgets, accédez à TODO 3 dans test / list / cars_list_bloc_test.dart et jetez un œil aux tests unitaires mis en œuvre dans ce projet. Ces tests unitaires garantissent que la structure de données que vous fournissez au widget est exacte.

Avant de commencer à écrire les scripts de test, il est bon de regarder à nouveau l’écran réel que vous testez. Dans test / base de données / mock_car_data_provider.dart, l’utilisateur a sélectionné la première voiture – la Hyundai Sonata 2017. Après le chargement de la liste, la voiture sélectionnée aura un fond bleu. Voir l’image ci-dessous:

Liste des voitures avec la carte sélectionnée surlignée en bleu

Votre premier test

Ouvert cars_list_page_test et ajoutez ces lignes à leurs endroits appropriés marqués par le numéro TODO:

testWidgets(
    "Cars are displayed with summary details, and selected car is highlighted blue.",
    (WidgetTester tester) async {
  // TODO 4: Inject and Load Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProvider());

  // TODO 5: Load & Sort Mock Data for Verification
  CarsList cars = await MockCarDataProvider().loadCars();
  cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases);

  ...
});

Injection de données de test

Cela injectera les données de test.

Ensuite, ajoutez ces lignes de code à TODO 6:

// TODO 6: Load and render Widget
await tester.pumpWidget(ListPageWrapper());
await tester.pump(Duration.zero);

Ici, pumpWidget rend et effectue une runApp d’un apatride ListPage widget enveloppé dans ListPageWrapper. Ensuite, vous appelez pump pour rendre le cadre sans délai. Cela prépare le widget pour les tests!

Remarque:
pumpWidget appels runApp, et déclenche également un cadre pour peindre l’application. Cela suffit si votre interface utilisateur et vos données sont toutes fournies immédiatement depuis l’application, ou je pourrais les appeler des données statiques. (c.-à-d. étiquettes et textes) Lorsque vous avez une structure (c.-à-d. liste, collections) avec des modèles de données répétés, pump devient essentiel pour déclencher une reconstruction, car le processus de chargement des donnéesrunApp.

Assurer la visibilité

Tout d’abord, assurez-vous que Carslist est dans la vue. Ajoutez ces lignes de code:

// TODO 7: Check Cars List's component's existence via key
final carListKey = find.byKey(Key(CARS_LIST_KEY));
expect(carListKey, findsOneWidget);

Dans cars_list_page.dart, vous verrez que l’arborescence des widgets identifie ListView avec une clé appelée CARS_LIST_KEY. findsOneWidget utilise un matcher pour localiser exactement un tel widget.

Ensuite, ajoutez cette fonction à TODO 8:

// TODO 8: Create a function to verify list's existence
void _verifyAllCarDetails(List carsList, WidgetTester tester) async {
  for (var car in carsList) {
    final carTitleFinder = find.text(car.title);
    final carPricePerDayFinder = find.text(PRICE_PER_DAY_TEXT.replaceFirst(
        WILD_STRING, car.pricePerDay.toStringAsFixed(2)));
    await tester.ensureVisible(carTitleFinder);
    expect(carTitleFinder, findsOneWidget);
    await tester.ensureVisible(carPricePerDayFinder);
    expect(carPricePerDayFinder, findsOneWidget);
  }
}

Les données fictives affichent un total de six voitures, mais vous ne voulez pas écrire de test pour chacune. Une bonne pratique consiste à utiliser un for boucle pour parcourir et vérifier chaque voiture de la liste.

Reportez-vous à la capture d’écran de l’application au début de ce didacticiel pour obtenir une image plus claire de ce que fait ce test. Il vérifie que le titre et le prix par jour s’affichent correctement. Ceci est possible grâce à une fonction appelée ensureVisible.

Tenir Commander et survoler ensureVisible pour voir sa description. Cette fonction aide le test à faire défiler l’arborescence des widgets jusqu’à ce qu’il trouve le widget attendu.
documentation de fonction

Remarque: Vous enveloppez un ListView dans un SingleChildScrollView pour que cela fonctionne cars_list_page.dart. Au moment de la rédaction, vous devez le faire pour que le test réussisse.

Théoriquement, un ListView contient également un élément déroulant pour permettre le défilement. Le test ne vérifie pas actuellement les images.

Le test des images coûte cher: cela nécessite d’obtenir des données du réseau et de vérifier des blocs de données. Cela peut entraîner une durée de test plus longue à mesure que le nombre de cas de test augmente.

Appelez la fonction que vous venez de créer pour vérifier les détails de la voiture:

// TODO 9: Call Verify Car Details function
_verifyAllCarDetails(cars.items, tester);

Essayez d’exécuter le test maintenant – oui, quatre tests ont réussi!

4 tests réussis

Widget Test de la page de liste de voitures avec sélection

Mais attendez, votre test n’est pas encore terminé. Rappelez-vous, lorsque vous sélectionnez une voiture, elle devrait avoir un fond bleu? Ensuite, vous testerez pour vous assurer que cela se produit.

Ajoutez ces lignes de code:

// TODO 10: Select a Car
carsListBloc.selectItem(1);

// TODO 11: Verify that Car is highlighted in blue
WidgetPredicate widgetSelectedPredicate = (Widget widget) =>
    widget is Card && widget.color == Colors.blue.shade200;
WidgetPredicate widgetUnselectedPredicate =
    (Widget widget) => widget is Card && widget.color == Colors.white;

expect(find.byWidgetPredicate(widgetSelectedPredicate), findsOneWidget);
expect(find.byWidgetPredicate(widgetUnselectedPredicate), findsNWidgets(5));

Voici ce que fait ce code:

  • TODO 10: Le testeur de widget tente de sélectionner l’ID de voiture 1.
  • TODO 11: Il crée ensuite deux prédicats: un pour vérifier que la carte sélectionnée a un fond bleu et un pour s’assurer que la carte non sélectionnée reste blanche.

Essayez d’exécuter le test maintenant. Vive, votre test passe toujours!

Tu vas très bien. Il est temps d’essayer quelques tests négatifs avant de terminer avec le test de la page des détails de la voiture.

Page Tests négatifs pour la liste des voitures

De TODO 12 à 14, ajoutez ces lignes de code:

testWidgets('Proper error message is shown when an error occurred',
    (WidgetTester tester) async {
  // TODO 12: Inject and Load Error Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProviderError());

  // TODO 13: Load and render Widget
  await tester.pumpWidget(ListPageWrapper());
  await tester.pump(Duration.zero);

  // TODO 14: Verify that Error Message is shown
  final errorFinder =
      find.text(ERROR_MESSAGE.replaceFirst(WILD_STRING, MOCK_ERROR_MESSAGE));
  expect(errorFinder, findsOneWidget);
});

Voici ce que vous faites avec ce code:

  • TODO 12-13: Vous l’avez déjà fait auparavant. La seule différence ici est que vous injectez MockCarDataProviderError, qui contient des données d’erreur factices.
  • TODO 14 Vérifiez que le message d’erreur s’affiche.

Prêt pour votre cinquième test? Exécutez-le et yay !!! Le cinquième test a réussi!

5 tests réussis

Vérification de la mise à jour de la vue

Il y a un dernier test que vous devez effectuer pour ce widget, qui est de vérifier que le widget met à jour sa vue si des données arrivent après avoir reçu une erreur.

Découvrez à quoi ressemble l’application:

Données d'erreur de la liste de contrôle

Apportez les modifications suivantes à TODO 15-20:

testWidgets('After encountering an error, and stream is updated, Widget is also updated.', (WidgetTester tester) async {
  // TODO 15: Inject and Load Error Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProviderError());

  // TODO 16: Load and render Widget
  await tester.pumpWidget(ListPageWrapper());
  await tester.pump(Duration.zero);

  // TODO 17: Verify that Error Message is shown
  final errorFinder =
      find.text(ERROR_MESSAGE.replaceFirst(WILD_STRING, MOCK_ERROR_MESSAGE));
  final retryButtonFinder = find.text(RETRY_BUTTON);

  expect(errorFinder, findsOneWidget);
  expect(retryButtonFinder, findsOneWidget);

  // TODO 18: Inject and Load Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProvider());
  await tester.tap(retryButtonFinder);

  // TODO 19: Reload Widget
  await tester.pump(Duration.zero);

  // TODO 20: Load and Verify Car Data
  CarsList cars = await MockCarDataProvider().loadCars();
  _verifyAllCarDetails(cars.items, tester);
});

Voici ce que fait le code ci-dessus:

  • TODO 15–17: Ce sont les mêmes que le dernier test que vous avez fait.
  • TODO 18: Injecte des données factices appropriées.
  • TODO 19: Recharge le widget.
  • TODO 20: Appelle la même fonction pour vérifier tous les détails de la voiture.

Il est temps d’exécuter le test. Exécutez-le maintenant, et… travail génial! Votre sixième test réussit!

6 tests réussis

Widget Test de la page Détails de la voiture pour la voiture désélectionnée

Enfin, passez au widget final: la page Détails de la voiture. Regardez à nouveau la page:

Écran des détails de la voiture

De TODO 21-24 en test/details/car_details_page_test.dart ajoutez ces lignes:

testWidgets('Unselected Car Details Page should be shown as Unselected', (WidgetTester tester) async {
  // TODO 21: Inject and Load Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProvider());
  await carsListBloc.loadItems();

  // TODO 22: Load & Sort Mock Data for Verification
  CarsList cars = await MockCarDataProvider().loadCars();
  cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases);

  // TODO 23: Load and render Widget
  await tester.pumpWidget(DetailsPageSelectedWrapper(2)); // Mercedes-Benz 2017
  await tester.pump(Duration.zero);

  // TODO 24: Verify Car Details
  final carDetailsKey = find.byKey(Key(CAR_DETAILS_KEY));
  expect(carDetailsKey, findsOneWidget);

  final pageTitleFinder = find.text(cars.items[1].title); // 2nd car in sorted list
  expect(pageTitleFinder, findsOneWidget);

  final notSelectedTextFinder = find.text(NOT_SELECTED_TITLE);
  expect(notSelectedTextFinder, findsOneWidget);

  final descriptionTextFinder = find.text(cars.items[1].description);
  expect(descriptionTextFinder, findsOneWidget);

  final featuresTitleTextFinder = find.text(FEATURES_TITLE);
  expect(featuresTitleTextFinder, findsOneWidget);

  var allFeatures = StringBuffer();
  cars.items[1].features.forEach((feature) {
    allFeatures.write('n' + feature + 'n');
  });

  final featureTextFinder = find.text(allFeatures.toString());
  await tester.ensureVisible(featureTextFinder);
  expect(featureTextFinder, findsOneWidget);

  final selectButtonFinder = find.text(SELECT_BUTTON);
  await tester.ensureVisible(selectButtonFinder);
  expect(selectButtonFinder, findsOneWidget);
});

Voici ce que vous avez accompli avec le code ci-dessus:

  • TODO 21–23: Encore une fois, vous injectez, chargez et triez les données, puis préparez et gonflez le widget.
  • TODO 24: Ouvert car_details_page.dart et vous trouverez un widget identifié par une clé, un titre de page, un titre désélectionné, une liste de fonctionnalités et un bouton Sélectionner. Tout ce code vous aide à les vérifier tous!

Exécutez les tests maintenant. Votre septième test a réussi!

7 tests réussis

Widget Testing Challenge

Votre défi consiste maintenant à terminer le reste par vous-même. Si vous êtes bloqué ou souhaitez comparer des solutions, cliquez simplement sur Révéler.

Objectifs:

  1. La page Détails de la voiture sélectionnée doit afficher une image statique Choisi texte en haut de la page. Lors de la visualisation d’une voiture sélectionnée, la page de détails doit être représentée correctement.
  2. Lorsque vous sélectionnez et désélectionnez une voiture, la page de détails doit être mise à jour en conséquence.

[spoiler]

Page Détails des tests pour les voitures sélectionnées

TODO 25–32:

testWidgets('Selected Car Details Page should be shown as Selected',
    (WidgetTester tester) async {
  // TODO 25: Inject and Load Mock Car Data
  carsListBloc.injectDataProviderForTest(MockCarDataProvider());
  await carsListBloc.loadItems();

  // TODO 26: Load and render Widget
  await tester
      .pumpWidget(DetailsPageSelectedWrapper(3)); // Hyundai Sonata 2017
  await tester.pump(Duration.zero);

  // TODO 27: Load Mock Data for Verification
  CarsList actualCarsList = await MockCarDataProvider().loadCars();
  List actualCars = actualCarsList.items;

  // TODO 28: First Car is Selected, so Verify that
  final carDetailsKey = find.byKey(Key(CAR_DETAILS_KEY));
  expect(carDetailsKey, findsOneWidget);

  final pageTitleFinder = find.text(actualCars[2].title);
  expect(pageTitleFinder, findsOneWidget);

  final notSelectedTextFinder = find.text(SELECTED_TITLE);
  expect(notSelectedTextFinder, findsOneWidget);

  final descriptionTextFinder = find.text(actualCars[2].description);
  expect(descriptionTextFinder, findsOneWidget);

  final featuresTitleTextFinder = find.text(FEATURES_TITLE);
  expect(featuresTitleTextFinder, findsOneWidget);

  var actualFeaturesStringBuffer = StringBuffer();
  actualCars[2].features.forEach((feature) {
    actualFeaturesStringBuffer.write('n' + feature + 'n');
  });

  final featuresTextFinder = find.text(actualFeaturesStringBuffer.toString());
  await tester.ensureVisible(featuresTextFinder);
  expect(featuresTextFinder, findsOneWidget);

  final selectButtonFinder = find.text(REMOVE_BUTTON);
  await tester.ensureVisible(selectButtonFinder);
  expect(selectButtonFinder, findsOneWidget);
});

Test that the Selected Car Updates the Widget

testWidgets('Selecting Car Updates the Widget', (WidgetTester tester) async { // TODO 29: Inject and Load Mock Car Data carsListBloc.injectDataProviderForTest(MockCarDataProvider()); await carsListBloc.loadItems(); // TODO 30: Load & Sort Mock Data for Verification CarsList cars = await MockCarDataProvider().loadCars(); cars.items.sort(carsListBloc.alphabetiseItemsByTitleIgnoreCases); // TODO 31: Load and render Widget for the first car await tester .pumpWidget(DetailsPageSelectedWrapper(2)); // Mercedes-Benz 2017 await tester.pump(Duration.zero); // TODO 32: Tap on Select and Deselect to ensure widget updates final selectButtonFinder = find.text(SELECT_BUTTON); await tester.ensureVisible(selectButtonFinder); await tester.tap(selectButtonFinder); await tester.pump(Duration.zero); final deselectButtonFinder = find.text(REMOVE_BUTTON); await tester.ensureVisible(deselectButtonFinder); await tester.tap(deselectButtonFinder); await tester.pump(Duration.zero); final newSelectButtonFinder = find.text(SELECT_BUTTON); await tester.ensureVisible(newSelectButtonFinder); expect(newSelectButtonFinder, findsOneWidget); });

9 tests réussis

[/spoiler]

Toutes nos félicitations! Tu es maintenant officiel Ambassadeur des tests de widgets, allez-y et diffusez la bonne nouvelle!

Prix ​​Flutter Widget Testing

Où aller en partant d’ici?

Téléchargez le projet final en cliquant sur le Télécharger les documents en haut ou en bas de ce didacticiel.

Pour vos prochaines étapes, développez vos connaissances en test Flutter en explorant le UI tests cookbook de l’équipe Flutter.

Amenez ensuite vos tests au niveau supérieur en explorant et en intégrant Mockito pour se moquer des services Web et des bases de données en direct.

Nous espérons que vous avez apprécié ce didacticiel. Si vous avez des questions ou des commentaires, veuillez vous joindre au forum de discussion ci-dessous!

Close Menu