flutter-add-widget-test

Implémenter un test au niveau du composant en utilisant `WidgetTester` pour vérifier le rendu de l'interface utilisateur et les interactions de l'utilisateur (appui, défilement, saisie de texte). À utiliser lors de la validation qu'un widget spécifique affiche les données correctes et répond aux événements comme prévu.

npx skills add https://github.com/flutter/skills --skill flutter-add-widget-test

Rédiger des tests de widgets Flutter

Table des matières

Configuration et paramétrage

Assurez-vous que l'environnement de test est correctement configuré avant de rédiger des tests de widgets.

  1. Ajoutez la dépendance flutter_test à la section dev_dependencies du fichier pubspec.yaml.
  2. Placez tous les fichiers de test dans le répertoire test/ à la racine du projet.
  3. Suffixez tous les noms de fichiers de test par _test.dart (par exemple, widget_test.dart).

Composants principaux

Utilisez les composants flutter_test suivants pour interagir avec et valider l'arborescence des widgets :

  • WidgetTester : L'interface principale pour construire et interagir avec les widgets dans l'environnement de test. Fournie automatiquement par la fonction testWidgets().
  • Finder : Localise les widgets dans l'environnement de test (par exemple, find.text('Submit'), find.byType(TextField), find.byKey(Key('submit_btn'))).
  • Matcher : Vérifie la présence ou l'état des widgets localisés par un Finder (par exemple, findsOneWidget, findsNothing, findsNWidgets(2), matchesGoldenFile).

Flux de travail : Implémenter un test de widget

Copiez la liste de contrôle suivante pour suivre la progression lors de l'implémentation d'un nouveau test de widget.

Progression des tâches

  • [ ] Étape 1 : Définir le test. Utilisez testWidgets('description', (WidgetTester tester) async { ... }).
  • [ ] Étape 2 : Construire le widget. Appelez await tester.pumpWidget(MyWidget()) pour afficher l'interface utilisateur. Enveloppez le widget dans un widget MaterialApp ou Directionality s'il nécessite des données directionnelles ou thématiques héritées.
  • [ ] Étape 3 : Localiser les éléments. Instanciez les objets Finder pour les widgets cibles.
  • [ ] Étape 4 : Vérifier l'état initial. Utilisez expect(finder, matcher) pour valider le rendu initial.
  • [ ] Étape 5 : Simuler les interactions. Exécutez des gestes ou des entrées (par exemple, await tester.tap(buttonFinder)).
  • [ ] Étape 6 : Reconstruire l'arborescence. Appelez await tester.pump() ou await tester.pumpAndSettle() pour traiter les changements d'état.
  • [ ] Étape 7 : Vérifier l'état mis à jour. Utilisez expect() pour valider l'interface utilisateur après l'interaction.
  • [ ] Étape 8 : Exécuter et valider. Exécutez flutter test test/your_test_file_test.dart.
  • [ ] Étape 9 : Boucle de rétroaction. Examinez la sortie du test -> identifiez les matchers défaillants -> ajustez la logique du widget ou les assertions de test -> relancez jusqu'à réussite.

Interaction et gestion d'état

Appliquez la logique conditionnelle suivante en fonction du type d'interaction ou de changement d'état en cours de test :

  • Si vous testez un rendu statique : Appelez await tester.pumpWidget() une seule fois, puis exécutez immédiatement les assertions expect().
  • Si vous testez des changements d'état standard (par exemple, des appuis de bouton) :
    1. Appelez await tester.tap(finder).
    2. Appelez await tester.pump() pour déclencher une reconstruction d'une seule image.
  • Si vous testez des animations, des transitions ou des mises à jour d'interface utilisateur asynchrones :
    1. Déclenchez l'action (par exemple, await tester.drag(finder, Offset(500, 0))).
    2. Appelez await tester.pumpAndSettle() pour pomper les images à plusieurs reprises jusqu'à ce qu'aucune autre image ne soit programmée (fin de l'animation).
  • Si vous testez l'entrée de texte : Appelez await tester.enterText(textFieldFinder, 'Input string').
  • Si vous testez des éléments dans une liste dynamique ou longue : Appelez await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder) pour vous assurer que le widget cible est rendu avant d'interagir avec lui.

Exemples

Implémentation de test de widget haute fidélité

Widget cible (lib/todo_list.dart) :

import 'package:flutter/material.dart';

class TodoList extends StatefulWidget {
  const TodoList({super.key});

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            TextField(controller: controller),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];
                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (_) => setState(() => todos.removeAt(index)),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

Implémentation du test (test/todo_list_test.dart) :

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/todo_list.dart';

void main() {
  testWidgets('Add and remove a todo item', (WidgetTester tester) async {
    // 1. Build the widget
    await tester.pumpWidget(const TodoList());

    // 2. Verify initial state
    expect(find.byType(ListTile), findsNothing);

    // 3. Enter text into the TextField
    await tester.enterText(find.byType(TextField), 'Buy groceries');

    // 4. Tap the add button
    await tester.tap(find.byType(FloatingActionButton));

    // 5. Rebuild the widget to reflect the new state
    await tester.pump();

    // 6. Verify the item was added
    expect(find.text('Buy groceries'), findsOneWidget);

    // 7. Swipe the item to dismiss it
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // 8. Build the widget until the dismiss animation ends
    await tester.pumpAndSettle();

    // 9. Verify the item was removed
    expect(find.text('Buy groceries'), findsNothing);
  });
}