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
- Comment utiliser cette compétence (Le workflow)
- Différences syntaxiques clés et pièges
- Tableau de correspondance Matcher vers Checks
- Matchers sans remplacement direct
- Stratégies de découverte
- Exemples
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:checkscommedev_dependencydanspubspec.yaml:dart pub add dev:checks - Supprimez
package:matchers'il est explicitement listé sousdev_dependencies(il est généralement inclus transitivement parpackage: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
expectouexpectLaterhé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 :
- 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
- Remplacez l'import générique
- Traduire les assertions : Réécrivez les appels
expectetexpectLaterhérités en syntaxechecken suivant les Différences syntaxiques clés et pièges et le Tableau de correspondance Matcher vers Checks. - Vérifier via le compilateur : Si vous migrez complètement, supprimez la ligne
import 'package:test/expect.dart';. Tout appelexpectnon 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 analyzePrê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 avertissementsunawaited_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 testSi un test échoue, examinez la sortie d'erreur extrêmement détaillée de
package:checkspour 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)ouexpect(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 pasoperator ==pour la comparaison élément par élément, utiliser.equalssur 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 fonctioncheckavant 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 argumentStringenRegExp(par exemple,matches(r'\d')correspondait à'1'). -
Package Checks :
.matchesPattern(pattern)traite un argumentStringcomme 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 lesSubject, prend un argument de moins et retourne un nouveauSubjectrepré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,throwsAse comportait de manière similaire pour les closures synchrones et les futures asynchrones quand enveloppés dansexpectouexpectLater. -
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 unSubject<E>de manière synchrone. Cela n'accepte pas d'argument callback ! Vous chaînez ou cascadez les expectations directement duSubject<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>()retourneFuture<void>. Puisque vous ne pouvez pas chaîner directement depuisFuture<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 retourneFuture<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 instancesRegExpsé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'objetRegExpexplicitement :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,
isTrueetisFalseeffectuaient 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 surSubject<bool>(non-nullable). Ils ne sont pas disponibles surSubject<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'uneMapcontenait une clé spécifique. -
Package Checks : Appeler
.contains(...)sur unSubject<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
QrEciValueest une représentation de type d'extension deint(par exemple,extension type const QrEciValue(int value) implements int), appeler.equals(3)sur unSubject<QrEciValue>échoue car3(unint) n'est pas assignable àQrEciValue. Un cast avecas intdéclenchera un avertissement d'analyse statique « Unnecessary cast » carQrEciValueimplémente statiquementint. - Correction : Spécifiez explicitement le paramètre de type générique sur la fonction
checkpour 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
dynamicdirectement 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
ListouMap:// 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)),
]);