Tester une application Flutter

1. Introduction

Flutter est un kit d'interface utilisateur Google qui permet de développer des applications esthétiques compilées de manière native pour les mobiles, le Web et les ordinateurs de bureau, à partir d'un seul codebase.

Dans cet atelier de programmation, vous allez développer et tester une application Flutter simple qui utilisera le package Provider pour gérer l'état.

Points abordés

  • Créer des tests de widgets en utilisant le framework de tests de widgets
  • Créer un test d'intégration pour évaluer l'interface utilisateur et les performances de l'application à l'aide de la bibliothèque integration_test
  • Tester des classes de données (fournisseurs) à l'aide de tests unitaires

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez commencer par développer une application simple contenant une liste d'éléments. Pour que vous puissiez passer directement aux tests, nous vous fournissons le code source. Cette application permet d'effectuer les opérations suivantes :

  • Ajouter les éléments aux favoris
  • Afficher la liste des favoris
  • Supprimer des éléments de la liste des favoris

Une fois l'application terminée, vous allez écrire les tests suivants :

  • Tests unitaires pour valider les opérations d'ajout et de suppression
  • Tests des widgets des pages d'accueil et de favoris
  • Tests de l'interface utilisateur et des performances de l'application entière à l'aide des tests d'intégration

GIF illustrant l'application exécutée sous Android

Qu'attendez-vous de cet atelier de programmation ?

Je suis novice en la matière et je voudrais avoir un bon aperçu. Je connais un peu le sujet, mais j'aimerais revoir certains points. Je recherche un exemple de code à utiliser dans mon projet. Je cherche des explications sur un point spécifique.

2. Configurer votre environnement de développement Flutter

Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.

Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :

  • Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur
  • Le simulateur iOS (les outils Xcode doivent être installés)
  • L'émulateur Android (doit être configuré dans Android Studio)
  • Un navigateur (Chrome est requis pour le débogage)
  • En tant qu'application de bureau Windows, Linux ou macOS. Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez le faire sous Windows pour accéder à la chaîne de compilation appropriée. Les exigences spécifiques aux systèmes d'exploitation sont détaillées sur docs.flutter.dev/desktop.

3. Premiers pas

Créer une application Flutter et modifier les dépendances

Cet atelier de programmation consiste à tester une application mobile Flutter. Vous allez commencer par la créer rapidement à l'aide des fichiers sources que vous aurez copiés et collés. Le reste de l'atelier porte sur l'apprentissage des différents types de tests.

a3c16fc17be25f6c.pngCréez une application Flutter simple à partir d'un modèle. Vous pouvez suivre les instructions dans Premiers pas avec votre première application Flutter ou celles des lignes de commande ci-dessous.

$ flutter create testing_app

a3c16fc17be25f6c.pngAjoutez des dépendances pub sur la ligne de commande. Pour une gestion facile de l'état, ajoutez provider :

$ cd testing_app
$ flutter pub add provider
Resolving dependencies...
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ nested 1.0.0
  path 1.8.2 (1.8.3 available)
+ provider 6.0.5
  test_api 0.4.16 (0.4.18 available)
Changed 2 dependencies!

Pour les tests autonomes du code Flutter sur vos appareils ou émulateurs ajoutez integration_test :

$ flutter pub add --dev --sdk=flutter integration_test
Resolving dependencies...
+ archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ crypto 3.0.2
+ file 6.1.4
+ flutter_driver 0.0.0 from sdk flutter
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ integration_test 0.0.0 from sdk flutter
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
+ platform 3.1.0
+ process 4.2.4
+ sync_http 0.3.1
  test_api 0.4.16 (0.4.18 available)
+ typed_data 1.3.1
+ vm_service 9.4.0 (11.0.1 available)
+ webdriver 3.0.1 (3.0.2 available)
Changed 12 dependencies!

Pour une API avancée permettant de tester les applications Flutter exécutées sur des appareils réels ou des émulateurs, ajoutez flutter_driver :

$ flutter pub add --dev --sdk=flutter flutter_driver
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test_api 0.4.16 (0.4.18 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!

Pour les outils de test généraux, ajoutez test :

$ flutter pub add --dev test
Resolving dependencies...
+ _fe_analyzer_shared 52.0.0
+ analyzer 5.4.0
  archive 3.3.2 (3.3.6 available)
+ args 2.3.2
  collection 1.17.0 (1.17.1 available)
+ convert 3.1.1
+ coverage 1.6.3
+ frontend_server_client 3.2.0
+ glob 2.1.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.5 (0.6.7 available)
+ logging 1.1.1
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ mime 1.0.4
+ node_preamble 2.0.1
+ package_config 2.1.0
  path 1.8.2 (1.8.3 available)
+ pool 1.5.1
+ pub_semver 2.1.3
+ shelf 1.4.0
+ shelf_packages_handler 3.0.1
+ shelf_static 1.1.1
+ shelf_web_socket 1.0.3
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.11
+ test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
+ test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
+ watcher 1.0.2
+ web_socket_channel 2.3.0
  webdriver 3.0.1 (3.0.2 available)
+ webkit_inspection_protocol 1.2.0
+ yaml 3.1.1
Changed 28 dependencies!

Pour prendre en charge la navigation dans les applications, ajoutez go_router :

$ flutter pub add go_router
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 6.0.4
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Changed 2 dependencies!

Les dépendance suivantes devraient avoir été ajoutées à pubspec.yaml :

Sous dependencies :

dependencies:
  provider: ^6.0.5
  go_router: ^6.0.4

Sous dev_dependencies :

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.22.0

a3c16fc17be25f6c.pngOuvrez le projet dans l'éditeur de code de votre choix, puis exécutez l'application. Vous pouvez aussi l'exécuter à l'aide de la ligne de commande suivante.

$ flutter run

4. Développer l'application

Vous allez maintenant créer l'application pour pouvoir la tester. Elle contient les fichiers suivants :

  • lib/models/favorites.dart (crée la classe de modèle pour la liste des favoris)
  • lib/screens/favorites.dart (crée la disposition de la liste des favoris)
  • lib/screens/home.dart (crée une liste d'éléments)
  • lib/main.dart (fichier principal dans lequel l'application démarre)

Créer le modèle Favorites dans le fichier lib/models/favorites.dart

a3c16fc17be25f6c.pngCréez un sous-répertoire models dans le répertoire lib, puis créez un fichier nommé favorites.dart. Dans ce fichier , ajoutez le code suivant :

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

Ajouter la page des favoris dans le fichier lib/screens/favorites.dart

a3c16fc17be25f6c.pngDans le répertoire lib, créez un sous-répertoire screens puis créez un fichier nommé favorites.dart. Dans ce fichier , ajoutez le code suivant :

lib/screens/favorites.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Ajouter la page d'accueil dans le fichier lib/screens/home.dart

a3c16fc17be25f6c.pngDans le répertoire lib/screens, créez un autre fichier que vous nommerez home.dart. Dans lib/screens/home.dart, ajoutez le code suivant :

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Remplacer le contenu du fichier lib/main.dart

a3c16fc17be25f6c.pngRemplacez le contenu du fichier lib/main.dart par le code suivant :

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

void main() {
  runApp(const TestingApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

class TestingApp extends StatelessWidget {
  const TestingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

L'application est maintenant terminée, mais n'a pas encore été testée.

a3c16fc17be25f6c.pngExécutez l'application. Elle doit ressembler à la capture d'écran ci-dessous :

b74f843e42a28b0f.png

L'application affiche une liste d'éléments. Pour ajouter un élément à vos favoris, appuyez sur l'icône correspondante en forme de cœur (de sorte que ce cœur soit rempli). Le bouton Favoris de la AppBar vous redirige vers un second écran qui contient la liste des favoris.

L'application est maintenant prête à être testée. Vous commencerez les tests à la prochaine étape.

5. Test unitaire du fournisseur

Vous commencerez par des tests unitaires du modèle favorites. Qu'est-ce qu'un test unitaire ? Un test unitaire permet de vérifier que chaque unité individuelle de logiciel (fonction, objet ou widget) exécute correctement la tâche prévue.

Tous les fichiers de test d'une application Flutter (hormis pour les tests d'intégration) sont placés dans le répertoire test.

Supprimer test/widget_test.dart

a3c16fc17be25f6c.pngAvant de commencer les tests, supprimez le fichier widget_test.dart. Vous allez ajouter vos propres fichiers de test.

Créer un fichier de test

Tout d'abord, vous allez tester la méthode add() dans le modèle Favorites pour vérifier qu'un nouvel élément est ajouté à la liste, et que celle-ci reflète bien cette modification. Par convention, la structure de répertoires dans le répertoire test reprend celle du répertoire lib. Les fichiers Dart portent le même nom, mais avec le suffixe _test.

a3c16fc17be25f6c.pngDans le répertoire test, créez d'abord un sous-répertoire models, puis dans celui-ci, un fichier favorites_test.dart contenant ce qui suit :

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });
  });
}

Le framework de test Flutter vous permet d'associer des tests similaires liés les uns aux autres dans un groupe. Il peut y avoir plusieurs groupes dans un seul fichier de test destinés à tester différentes parties du fichier correspondant dans le répertoire lib.

La méthode test() utilise deux paramètres de position : la description du test et le callback dans lequel vous écrivez en fait le test.

a3c16fc17be25f6c.pngTestez la suppression d'un élément de la liste. Insérez le test suivant dans le même groupe Testing App Provider :

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

Exécuter le test

a3c16fc17be25f6c.pngDans la ligne de commande, accédez au répertoire racine du projet et saisissez la commande suivante :

$ flutter test test/models/favorites_test.dart

Si tout fonctionne correctement, vous devriez voir un message de ce type :

00:06 +2: All tests passed!

Fichier de test complet : test/models/favorites_test.dart.

Pour en savoir plus sur les tests unitaires, lisez l'introduction aux tests unitaires.

6. Test de widgets

Au cours de cette étape, vous allez ajouter du code pour tester les widgets. Les tests de widgets sont propres à Flutter, qui permet de tester chaque widget de façon isolée. Cette étape permet de tester les écrans HomePage et FavoritesPage individuellement.

Les tests de widgets font appel à la fonction testWidget() au lieu de la fonction test(). Comme la fonction test(), la fonction testWidget() accepte deux paramètres : description, et callback. Ce dernier accepte WidgetTester comme argument.

Les tests de widgets utilisent TestFlutterWidgetsBinding, une classe qui fournit les mêmes ressources à vos widgets que dans une application en cours d'exécution (par exemple, des informations sur la taille de l'écran, la possibilité de planifier des animations, etc.), mais sans exécuter l'application elle-même. Au lieu de cela, un environnement virtuel est utilisé pour instancier le widget, puis tester les résultats. Ici, pumpWidget lance le processus en demandant au framework d'installer et de mesurer un widget particulier comme il le ferait dans une application.

Le framework de test de widgets fournit des outils de recherche pour trouver les widgets (par exemple, text(), byType(), byIcon().) et des outils de mise en correspondance pour vérifier les résultats.

Commencez par tester le widget HomePage.

Créer un fichier de test

Le premier test consiste à vérifier si la HomePage défile correctement.

a3c16fc17be25f6c.pngDans le répertoire test, créez un fichier que vous nommerez home_test.dart et dans lequel vous ajouterez le code suivant :

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

La fonction createHomeScreen() permet de créer une application qui charge le widget à tester dans un MaterialApp, encapsulé dans un ChangeNotifierProvider. Dans l'arborescence des widgets, le widget HomePage doit toujours figurer au-dessous de ces deux widgets pour hériter de ceux-ci et accéder aux données qu'ils proposent. Cette fonction est transmise en tant que paramètre à la fonction pumpWidget().

Vérifiez ensuite si le framework peut trouver une ListView affichée à l'écran.

a3c16fc17be25f6c.pngAjoutez l'extrait de code suivant à home_test.dart :

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

Exécuter le test

Exécutez d'abord le test comme vous le feriez pour un test unitaire, à l'aide de la commande suivante :

$ flutter test test/home_test.dart

Ce test devrait être rapide et produire un message semblable à ce qui suit :

00:02 +2: All tests passed!

Vous pouvez également effectuer les tests de widgets avec un appareil ou un émulateur, ce qui permet d'observer l'exécution du test et de redémarrer à chaud.

a3c16fc17be25f6c.pngBranchez votre appareil ou démarrez l'émulateur. Vous pouvez aussi exécuter le test en tant qu'application de bureau.

a3c16fc17be25f6c.pngÀ partir de la ligne de commande, accédez au répertoire racine du projet et saisissez la commande suivante :

$ flutter run test/home_test.dart

Vous devrez peut-être sélectionner l'appareil sur lequel exécuter le test. Dans ce cas, suivez les instructions et choisissez un appareil :

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"):

Si tout fonctionne, vous devriez obtenir un résultat semblable à celui-ci :

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

Vous allez ensuite modifier le fichier de test et appuyer sur Shift + R pour redémarrer l'application et exécuter à nouveau tous les tests. N'interrompez pas l'application.

a3c16fc17be25f6c.pngAjoutez d'autres tests au groupe qui teste les widgets Homepage. Copiez le test suivant dans votre fichier :

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

Ce test permet de vérifier que lorsque l'utilisateur appuie sur l'IconButton, le cœur vide (Icons.favorite_border) devient plein (Icons.favorite), puis redevient vide (Icons.favorite_border) si l'utilisateur appuie de nouveau dessus.

a3c16fc17be25f6c.pngAppuyez sur Shift + R. L'application redémarre à chaud et tous les tests sont à nouveau exécutés.

Fichier de test complet : test/home_test.dart.

a3c16fc17be25f6c.pngSuivez la même procédure pour tester la FavoritesPage avec le code suivant, puis exécutez le test.

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

Ce test permet de vérifier si un élément disparaît lorsqu'un utilisateur appuie sur le bouton de fermeture (supprimer).

Pour en savoir plus sur les tests de widgets, consultez les ressources ci-dessous :

7. Tester l'interface utilisateur de l'application via des tests d'intégration

Les tests d'intégration permettent de tester la façon dont des éléments individuels d'une application fonctionnent ensemble globalement. Le package integration_test est utilisé pour effectuer des tests d'intégration dans Flutter. Il s'agit de la version Flutter de Selenium WebDriver, Protractor, Espresso, ou Earl Gray. Le package utilise flutter_driver en interne pour effectuer le test sur un appareil.

L'écriture de tests d'intégration dans Flutter ressemble à l'écriture des tests de widget, à la différence que les tests d'intégration sont exécutés sur un "appareil cible", qui peut être un appareil mobile, un navigateur ou une application de bureau.

Écrire le test

a3c16fc17be25f6c.pngDans le répertoire racine du projet, créez d'abord un sous-répertoire nommé integration_test. Ensuite, créez un fichier app_test.dart dans ce sous-répertoire.

integration_test/app_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

Exécuter le test

a3c16fc17be25f6c.pngBranchez votre appareil ou démarrez l'émulateur. Vous pouvez aussi exécuter le test en tant qu'application de bureau.

a3c16fc17be25f6c.pngDans la ligne de commande, accédez au répertoire racine du projet et saisissez la commande suivante :

$ flutter test integration_test/app_test.dart

Si tout fonctionne, vous devriez obtenir un résultat semblable à celui-ci :

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart
00:26 +1: All tests passed!

8. Tester les performances de l'application avec Flutter Driver

Écrire un test de performances

Dans le répertoire "integration_test", créez un nouveau fichier de test nommé perf_test.dart avec le contenu suivant :

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

La fonction ensureInitialized() vérifie si le pilote d'intégration est initialisé et le réinitialise si nécessaire. Il est utile de définir framePolicy sur fullyLive pour tester du code animé.

Ce test fait d'abord défiler la liste des éléments très rapidement, puis toute la page vers le haut. La fonction traceAction() enregistre les actions et génère un résumé de la chronologie.

Enregistrer les performances

Pour consigner les résultats, créez un répertoire nommé test_driver puis, dans ce répertoire, créez un fichier nommé perf_driver.dart et ajoutez-y le code suivant :

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

Exécuter le test

a3c16fc17be25f6c.pngBranchez votre appareil ou démarrez l'émulateur.

a3c16fc17be25f6c.pngDans la ligne de commande, accédez au répertoire racine du projet et saisissez la commande suivante :

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

Si tout fonctionne, vous devriez obtenir un résultat semblable à celui-ci :

Running "flutter pub get" in testing_app...
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

Une fois le test réussi, vous trouverez deux fichiers dans le répertoire de build à la racine du projet :

  1. scrolling_summary.timeline_summary.json contient le résumé. Ouvrez-le dans n'importe quel éditeur de texte pour le consulter.
  2. scrolling_summary.timeline.json contient les données complètes de la chronologie.

Pour en savoir plus sur les tests d'intégration, consultez ces ressources :

9. Félicitations !

Vous avez terminé cet atelier de programmation et appris plusieurs manières de tester une application Flutter.

Connaissances acquises

  • Tester les fournisseurs via des tests unitaires
  • Tester des widgets en utilisant le framework de test de widgets
  • Tester l'interface utilisateur de l'application via des tests d'intégration
  • Tester les performances d'une application via des tests d'intégration

Pour en savoir plus sur les tests dans Flutter, consultez les pages suivantes :