dart-migrate-to-checks-package

Par flutter · skills

Remplacez l'utilisation de `expect` et des fonctions similaires de `package:matcher` par leurs équivalents dans `package:checks`.

npx skills add https://github.com/flutter/skills --skill dart-migrate-to-checks-package

Migration des tests Dart vers Package Checks

Utilisez cette compétence quand vous devez migrer une suite de tests Dart de la bibliothèque d'assertions héritée package:matcher (exportée par défaut depuis package:test/test.dart) vers la bibliothèque d'assertions moderne, type-safe et lisible package:checks.

Contenu


Quand utiliser cette compétence

  • Quand on vous demande de « migrer les tests vers checks », « utiliser package:checks », ou « moderniser les assertions de test ».
  • Lors de la mise à jour de suites de tests héritées où la sécurité des types statiques, une meilleure autocomplétion dans les IDE et des diagnostics d'erreur très détaillés sont souhaités.

Comment utiliser cette compétence (Le workflow)

Suivez ce workflow structuré pour migrer de manière sûre et systématique une suite de tests :

1. Configuration des dépendances

  • Ajoutez package:checks comme dev_dependency dans pubspec.yaml :
    dart pub add dev:checks
  • Supprimez package:matcher s'il est explicitement listé sous dev_dependencies (il est généralement inclus transitivement par package:test, ce qui va bien).

2. Identifier et planifier les fichiers cibles

  • Utilisez les motifs grep dans Stratégies de découverte pour localiser tous les fichiers de test contenant des appels expect ou expectLater hérités.
  • Décidez si vous migrez les fichiers complètement ou progressivement.

3. Migrer un fichier (Progressivement ou complètement)

Pour tout fichier de test cible :

  1. Mise à jour des imports :
    • Remplacez l'import générique import 'package:test/test.dart'; par :
      import 'package:test/scaffolding.dart';
      import 'package:checks/checks.dart';
    • Pour une migration progressive : Si vous ne voulez migrer que certains cas de test dans le fichier, ou migrer étape par étape, ajoutez :
      import 'package:test/expect.dart'; // Permet temporairement les expect() hérités
  2. Traduire les assertions : Réécrivez les appels expect et expectLater hérités en syntaxe check en suivant les Différences syntaxiques clés et pièges et le Tableau de correspondance Matcher vers Checks.
  3. Vérifier via le compilateur : Si vous migrez complètement, supprimez la ligne import 'package:test/expect.dart';. Tout appel expect non migré restant apparaîtra immédiatement comme erreur du compilateur, les rendant faciles à trouver et corriger.

4. Vérification et boucles de retour

  • Analyse statique : Exécutez l'analyse statique sur le package cible :
    dart analyze

    Prêtez une attention particulière aux paramètres de type générique sur .isA<Type>() et assurez-vous que les expectations asynchrones sont correctement attendues (vérifiez les avertissements unawaited_futures).

  • Exécuter les tests : Exécutez les tests pour vérifier à la fois le comportement et la logique d'exécution correcte des assertions :
    dart test

    Si un test échoue, examinez la sortie d'erreur extrêmement détaillée de package:checks pour diagnostiquer si le test échoue réellement ou si l'expectation a été traduite incorrectement.


Différences syntaxiques clés et pièges

[!IMPORTANT] Une traduction ligne par ligne peut parfois introduire des bugs subtils ou des faux succès. Examinez toujours attentivement ces différences clés :

1. Piège d'égalité des collections (equals vs deepEquals)

  • Matcher hérité : expect(actual, expected) ou expect(actual, equals(expected)) effectuait une vérification d'égalité profonde si les arguments étaient des collections (Lists, Maps, Sets).

  • Package Checks : .equals(expected) correspond strictement à operator ==. Puisque les collections Dart n'écrasent pas operator == pour la comparaison élément par élément, utiliser .equals sur une collection vérifiera l'identité et échouera presque certainement à l'exécution.

  • Correction : Vous devez remplacer les assertions d'égalité des collections par .deepEquals(expected).

    // AVANT (Matcher)
    expect(myList, [1, 2, 3]);
    
    // APRÈS (Checks)
    check(myList).deepEquals([1, 2, 3]);

2. Le paramètre reason est maintenant because

  • Matcher hérité : L'explication a été passée comme argument nommé de fin reason à expect :
    expect(actual, expectation, reason: 'Explication');
  • Package Checks : L'explication est passée comme argument nommé because à la fonction check avant le sujet réel :
    check(because: 'Explication', actual).expectation();

3. Correspondance des expressions régulières (matches vs matchesPattern)

  • Matcher hérité : Le matcher matches(pattern) convertissait automatiquement un argument String en RegExp (par exemple, matches(r'\d') correspondait à '1').

  • Package Checks : .matchesPattern(pattern) traite un argument String comme un motif de chaîne littérale.

  • Correction : Pour faire correspondre à l'aide d'une expression régulière, vous devez passer explicitement un objet RegExp :

    // AVANT (Matcher)
    expect(someString, matches(r'\d+'));
    
    // APRÈS (Checks)
    check(someString).matchesPattern(RegExp(r'\d+'));

4. Extraction de propriété (TypeMatcher.having vs .has)

  • Matcher hérité : Les expectations de champs/propriétés chaînées utilisaient TypeMatcher.having(feature, description, matcher) :
    expect(actual, isA<Person>().having((p) => p.name, 'name', startsWith('A')));
  • Package Checks : L'extension .has(feature, description) est disponible sur tous les Subject, prend un argument de moins et retourne un nouveau Subject représentant cette propriété. Vous chaînez les expectations directement :
    check(actual).isA<Person>().has((p) => p.name, 'name').startsWith('A');

5. throws synchrone vs asynchrone

  • Matcher hérité : Dans package:matcher, throwsA se comportait de manière similaire pour les closures synchrones et les futures asynchrones quand enveloppés dans expect ou expectLater.

  • Package Checks : L'expectation .throws<E>() se comporte différemment et a des types de retour différents selon que le sujet est synchrone ou asynchrone :

    • Synchrone (Subject<T Function()>): .throws<E>() retourne un Subject<E> de manière synchrone. Cela n'accepte pas d'argument callback ! Vous chaînez ou cascadez les expectations directement du Subject<E> retourné :

      // OUI (Chaînage synchrone)
      check(() => triggerSyncError()).throws<ArgumentError>()
        ..has((e) => e.message, 'message').equals('invalid input');
      
      // NON (Passer un callback à throws synchrone causera une erreur compilateur !)
      check(() => triggerSync").throws<ArgumentError>((it) => ...); // ERREUR !
    • Asynchrone (Subject<Future<T>>): .throws<E>() retourne Future<void>. Puisque vous ne pouvez pas chaîner directement depuis Future<void>, cela nécessite un callback d'inspection :

      // OUI (Callback asynchrone)
      await check(triggerAsyncError()).throws<ArgumentError>((it) => it
        ..has((e) => e.message, 'message').equals('invalid input'));
    • Piège crucial : Essayer de chaîner les expectations directement après un .throws<E>() asynchrone attendu (par exemple, await check(future).throws<E>().equals(...)) échouera à la compilation car il retourne Future<void>.

6. Égalité RegExp / Pattern

  • Matcher hérité : Dans package:matcher, expect(myPattern, equals(RegExp('Hello'))) fonctionnait car les règles de comparaison du matcher géraient les instances RegExp.
  • Package Checks : .equals() utilise l'égalité Dart == stricte. Puisque les instances RegExp séparées ne satisfont pas ==, utiliser .equals() échouera à l'exécution.
  • Correction : Utilisez le raffinement de type .isA<RegExp>() avec des cascades pour affirmer les propriétés de l'objet RegExp explicitement :
    check(myPattern).isA<RegExp>()
      ..has((r) => r.pattern, 'pattern').equals('Hello')
      ..has((r) => r.isMultiLine, 'isMultiLine').isTrue();

7. Sécurité stricte des booléens nullables (bool?)

  • Matcher hérité : Statiquement, isTrue et isFalse effectuaient des vérifications dynamiques lâches à l'exécution, qui acceptaient silencieusement les booléens nullables (bool?).
  • Package Checks : .isTrue() et .isFalse() sont définis strictement sur Subject<bool> (non-nullable). Ils ne sont pas disponibles sur Subject<bool?>.
  • Correction : Pour les champs déclarés comme bool?, vous devez soit raffiner le sujet (par exemple, .isNotNull().isTrue()) ou simplement utiliser .equals(true) et .equals(false) qui sont génériques et fonctionnent sur tous les types :
    // Si options.flagOutdated est un bool?
    check(options.flagOutdated).equals(true);
    check(options.flagOutdated).equals(false);

8. Containment des clés de Map (containsKey vs contains)

  • Matcher hérité : Dans package:matcher, contains(key) était utilisé pour affirmer qu'une Map contenait une clé spécifique.

  • Package Checks : Appeler .contains(...) sur un Subject<Map> n'est pas défini et échouera à la compilation.

  • Correction : Utilisez plutôt le matcher spécifique aux maps .containsKey(key) :

    // AVANT (Matcher)
    expect(myMap, contains('my_key'));
    
    // APRÈS (Checks)
    check(myMap).containsKey('my_key');

9. Paramètres génériques explicites pour les types d'extension

  • Matcher hérité : expect(extensionTypeConst, 3) compilait en raison de l'égalité dynamique lâche.
  • Package Checks : Si QrEciValue est une représentation de type d'extension de int (par exemple, extension type const QrEciValue(int value) implements int), appeler .equals(3) sur un Subject<QrEciValue> échoue car 3 (un int) n'est pas assignable à QrEciValue. Un cast avec as int déclenchera un avertissement d'analyse statique « Unnecessary cast » car QrEciValue implémente statiquement int.
  • Correction : Spécifiez explicitement le paramètre de type générique sur la fonction check pour forcer checks à le traiter comme le type primitif :
    // OUI (Type-safe et sans avertissement)
    check<int>(QrEciValue.iso8859_1).equals(3);

10. Cast de recherche de Map / JSON dynamique

  • Matcher hérité : Le typage dynamique lâche permettait de comparer les recherches JSON imbriquées typées statiquement comme dynamic directement contre les listes ou maps.
  • Package Checks : La sécurité de type stricte rejette l'assignation implicite de dynamic à Iterable<Object?> dans .deepEquals(...).
  • Correction : Castez statiquement le résultat de recherche dynamique vers une List ou Map :
    // OUI (Cast explicite vers List)
    check(myIterable).deepEquals(json['data']['items'] as List);

Tableau de correspondance Matcher vers Checks

Utilisez ce tableau comme référence rapide pour les remplacements de matchers directs :

Matcher hérité Équivalent Package Checks Notes
expect(actual, expected) check(actual).equals(expected) Utilisez .deepEquals pour les collections !
expect(actual, equals(expected)) check(actual).equals(expected) Utilisez .deepEquals pour les collections !
isA<T>() check(actual).isA<T>() Le chaînage est supporté directement
same(expected) check(actual).identicalTo(expected) Vérifie l'identité
anyElement(matcher) check(iterable).any(conditionCallback) Par exemple check(list).any((e) => e.equals(1))
everyElement(matcher) check(iterable).every(conditionCallback) Par exemple check(list).every((e) => e.isGreaterThan(0))
hasLength(expected) check(actual).length.equals(expected) Fonctionne sur String, Map, Iterable, etc.
isNot(matcher) check(actual).not(conditionCallback) Par exemple check(val).not((it) => it.equals(5))
contains(element) check(actual).contains(element) Fonctionne sur String, Iterable (utilisez containsKey pour Map !)
contains(key) (sur une Map) check(map).containsKey(key) Containment des clés de map
startsWith(prefix) check(string).startsWith(prefix) String uniquement
endsWith(suffix) check(string).endsWith(suffix) String uniquement
isEmpty check(actual).isEmpty() Fonctionne sur String, Map, Iterable
isNotEmpty check(actual).isNotEmpty() Fonctionne sur String, Map, Iterable
isNull check(actual).isNull()
isNotNull check(actual).isNotNull()
isTrue / true check(actual).isTrue() Fonctionne sur bool non-nullable uniquement
isFalse / false check(actual).isFalse() Fonctionne sur bool non-nullable uniquement
completion(matcher) await check(future).completes(conditionCallback) Doit être attendu !
throwsA(matcher) await check(future).throws<Type>() Doit être attendu !
emits(value) await check(streamQueue).emits(conditionCallback) Doit être attendu !
emitsThrough(value) await check(streamQueue).emitsThrough(conditionCallback) Doit être attendu !
stringContainsInOrder(list) check(string).containsInOrder(list) String uniquement
pairwiseCompare(...) check(actual).pairwiseMatches(...)

Matchers sans remplacement direct

Certains matchers hérités n'ont pas d'équivalent un-à-un dans package:checks en raison du nettoyage de l'API. Utilisez ces contournements standard :

1. Matchers d'erreur spécifiques

  • Hérité : throwsArgumentError, throwsStateError, throwsUnsupportedError, etc.
  • Checks : Utilisez .throws<T>() avec le type d'erreur spécifique :
    await check(triggerError()).throws<ArgumentError>();

2. Le matcher anything

  • Hérité : expect(actual, anything)
  • Checks : Passez un callback de condition vide (_) {} quand une condition est syntaxiquement requise :
    await check(someFuture).completes((_) {});

3. Bascules numériques spécifiques

  • Hérité : isPositive, isNegative, isZero, isNonPositive, isNonNegative, isNonZero
  • Checks : Utilisez les expectations comparatives explicites :
    • isPositive $\rightarrow$ isGreaterThan(0)
    • isNegative $\rightarrow$ isLessThan(0)
    • isZero $\rightarrow$ equals(0)
    • isNonNegative $\rightarrow$ isGreaterOrEqual(0)

4. Plages numériques

  • Hérité : inClosedOpenRange(min, max), inInclusiveRange(min, max), etc.
  • Checks : Chaînez les limites en utilisant l'opérateur cascade (..) :
    check(actualValue)
      ..isGreaterOrEqual(min)
      ..isLessThan(max);

Écriture d'expectations personnalisées (Remplacement des matchers personnalisés)

Lors de la migration depuis une base de code héritée, vous pouvez rencontrer des sous-classes Matcher personnalisées. Dans package:checks, les assertions personnalisées sont implémentées comme des méthodes extension sur Subject<T>.

Pour écrire des expectations personnalisées, vous devez importer l'API de contexte checks :

import 'package:checks/context.dart';

1. Expectations personnalisées simples (utilisant expect)

Utilisez context.expect pour vérifier une propriété et retourner un Rejection en cas d'erreur :

extension CustomPersonChecks on Subject<Person> {
  void isAdult() {
    context.expect(
      () => ['is an adult (age >= 18)'],
      (actual) {
        if (actual.age >= 18) return null; // Passage
        return Rejection(
          which: ['is only ${actual.age} years old'],
        );
      },
    );
  }
}

2. Extraction de propriété imbriquée (utilisant nest ou has)

Pour extraire une propriété et permettre des vérifications chaînées ultérieures, utilisez nest ou l'aide plus simple has :

  • Utilisant has (Recommandé pour l'accès à un champ simple et non échouable) :
    extension CustomPersonChecks on Subject<Person> {
      Subject<Address> get address => has((p) => p.address, 'address');
    }
  • Utilisant nest (Pour l'extraction de propriété qui peut échouer ou rejeter) :
    extension CustomPersonChecks on Subject<Person> {
      Subject<String> get ssn => context.nest(
        'has a valid SSN',
        (actual) {
          final ssnValue = actual.ssn;
          if (ssnValue == null) {
            return Extracted.rejection(which: ['has no SSN']);
          }
          return Extracted.value(ssnValue);
        },
      );
    }

3. Expectations personnalisées asynchrones

Si l'expectation est asynchrone (par exemple vérifier un Future ou Stream), utilisez context.expectAsync ou context.nestAsync et retournez le Future résultant :

extension CustomFutureChecks<T> on Subject<Future<T>> {
  Future<void> completesNormally() {
    return context.expectAsync(
      () => ['completes without throwing'],
      (actual) async {
        try {
          await actual;
          return null; // Passage
        } catch (e) {
          return Rejection(which: ['threw $e']);
        }
      },
    );
  }
}

Stratégies de découverte

Exécutez ces commandes dans le terminal pour identifier les matchers hérités et les fichiers nécessitant une migration :

# 1. Trouver tous les fichiers de test contenant des expect() ou expectLater() hérités
grep -rn "expect(" test/
grep -rn "expectLater(" test/

# 2. Trouver les pièges potentiels d'égalité des collections (listes ou maps littérales)
grep -rn "expect(.*, \[" test/
grep -rn "expect(.*, {" test/

# 3. Trouver les appels matches() (nécessitent conversion vers RegExp + matchesPattern)
grep -rn "matches(" test/

# 4. Trouver les appels TypeMatcher.having() hérités (qui nécessitent conversion vers .has())
grep -rn "having(" test/

Exemples

Assertions basiques

Avant (Matcher):

expect(someValue, isNotNull);
expect(result, isTrue, reason: 'should be successful');
expect(myString, startsWith('hello'));

Après (Checks):

check(someValue).isNotNull();
check(because: 'should be successful', result).isTrue();
check(myString).startsWith('hello');

Collections et égalité profonde

Avant (Matcher):

expect(items, [1, 2, 3]);
expect(configMap, equals({'port': 8080}));

Après (Checks):

check(items).deepEquals([1, 2, 3]);
check(configMap).deepEquals({'port': 8080});

Chaînage et cascades

Avant (Matcher):

expect(someString, allOf([
  startsWith('a'),
  contains('b'),
  endsWith('c'),
]));

Après (Checks):

check(someString)
  ..startsWith('a')
  ..contains('b')
  ..endsWith('c');

Correspondance complexe de propriété (has)

Avant (Matcher):

expect(response, isA<Response>()
    .having((r) => r.statusCode, 'statusCode', 200)
    .having((r) => r.body, 'body', contains('success')));

Après (Checks):

check(response).isA<Response>()
  ..has((r) => r.statusCode, 'statusCode').equals(200)
  ..has((r) => r.body, 'body').contains('success');

Futures asynchrones

Avant (Matcher):

expect(fetchData(), completes);
expect(fetchData(), completion(equals('data')));
expect(failingCall(), throwsA(isA<StateError>()));

Après (Checks):

await check(fetchData()).completes();
await check(fetchData()).completes((it) => it.equals('data'));
await check(failingCall()).throws<StateError>();

Streams asynchrones

Avant (Matcher):

var queue = StreamQueue(Stream.fromIterable([1, 2, 3]));
await expectLater(queue, emitsInOrder([1, 2, 3]));

Après (Checks):

var queue = StreamQueue(Stream.fromIterable([1, 2, 3]));
await check(queue).inOrder([
  (s) => s.emits((e) => e.equals(1)),
  (s) => s.emits((e) => e.equals(2)),
  (s) => s.emits((e) => e.equals(3)),
]);

Skills similaires