SKILL.md
只读
名称
dart-migrate-to-checks-package
描述
将 Dart 测试代码中来自旧版 `package:matcher` 的 `expect` 及相关断言函数,重构迁移为现代、类型安全的 `package:checks` 等效写法。
将 Dart 测试迁移至 Package Checks
当需要把 Dart 测试套件从传统的 package:matcher(即 package:test/test.dart 默认导出的测试断言库)迁移到现代、静态类型安全且可读性更高的 package:checks 断言库时,请使用此 Skill。
目录
何时使用此 Skill
- 当收到“将测试迁移到 checks”、“使用 package:checks”或“现代化测试断言”等重构要求时。
- 当更新旧版测试套件,希望获得静态类型安全、更友好的 IDE 智能补全以及更详尽的测试失败诊断信息时。
如何使用此 Skill(工作流)
请遵循以下结构化工作流,安全、系统地完成测试套件迁移:
1. 配置项目依赖
- 在
pubspec.yaml的dev_dependencies中添加package:checks:dart pub add dev:checks - 如果
pubspec.yaml的dev_dependencies中显式声明了package:matcher,可直接移除(它通常会被package:test传递依赖进来,无需显式声明)。
2. 梳理与规划目标文件
- 使用 查找与定位策略 章节中的 grep 正则模式,找出所有包含旧版
expect或expectLater调用的测试文件。 - 根据项目情况,评估选择一次性全量迁移还是按文件/用例增量迁移。
3. 迁移单个测试文件(增量或全量)
针对任意目标测试文件:
- 更新 Import 导入语句:
- 将原有的
import 'package:test/test.dart';替换为:import 'package:test/scaffolding.dart'; import 'package:checks/checks.dart'; - 增量迁移场景:如果你只想迁移文件中的部分测试用例,或希望分步平滑迁移,可暂时保留或添加:
import 'package:test/expect.dart'; // 临时允许继续使用旧版 expect()
- 将原有的
- 改写断言语句:参照 核心语法差异与避坑指南 和 Matcher 到 Checks 映射表,将旧版
expect和expectLater重构为check链式语法。 - 利用编译器验证:若是全量迁移,直接删掉
import 'package:test/expect.dart';这一行。此时文件中任何残留的旧版expect都会立即报编译错误,方便快速排查定位。
4. 验证与反馈循环
- 静态代码分析:在目标 Package 根目录运行 Dart 静态分析:
重点检查dart analyze.isA<Type>()上的泛型类型参数是否匹配,并确认所有异步断言(Expectations)都已加上await(关注是否有unawaited_futures分析警告)。 - 运行测试套件:执行测试,校验功能逻辑与断言运行时逻辑是否均无误:
若有测试用例失败,仔细分析dart testpackage:checks极为详尽的失败日志,排查是业务逻辑本身报错还是断言转换写法有误。
核心语法差异与避坑指南
[!IMPORTANT]
逐行机械替换容易引入隐蔽的 Bug 或导致测试“虚假通过”(False Passes)。请务必仔细复核以下关键语法差异:
1. 集合相等坑点(equals vs deepEquals)
- 旧版 Matcher:如果传入的参数是集合(List、Map、Set),
expect(actual, expected)或expect(actual, equals(expected))会默认进行深度相等比对(Deep equality check)。 - Package Checks:
.equals(expected)严格对应 Dart 的operator ==。由于 Dart 原生集合并没有重写operator ==来比对内部元素,在集合上直接调用.equals只会检查是否为同一对象引用,在运行时几乎必然断言失败。 - 修复方案:对于集合相等的断言,必须替换为
.deepEquals(expected)。// 迁移前 (Matcher) expect(myList, [1, 2, 3]); // 迁移后 (Checks) check(myList).deepEquals([1, 2, 3]);
2. reason 参数改为了 because
- 旧版 Matcher:自定义失败说明文本是作为尾部命名参数
reason传给expect的:expect(actual, expectation, reason: 'Explanation'); - Package Checks:失败说明文本改为了命名参数
because,且需要传给check函数本身(位于被测对象 之前):check(because: 'Explanation', actual).expectation();
3. 正则表达式匹配(matches vs matchesPattern)
- 旧版 Matcher:
matches(pattern)会自动将传入的String参数转为RegExp(例如matches(r'\d')可以匹配字符串'1')。 - Package Checks:
.matchesPattern(pattern)会将传入的String参数直接按普通的字面量字符串匹配。 - 修复方案:如果需要正则匹配,必须显式传入
RegExp实例对象:// 迁移前 (Matcher) expect(someString, matches(r'\d+')); // 迁移后 (Checks) check(someString).matchesPattern(RegExp(r'\d+'));
4. 属性提取(TypeMatcher.having vs .has)
- 旧版 Matcher:链式字段/属性断言使用的是
TypeMatcher.having(feature, description, matcher):expect(actual, isA<Person>().having((p) => p.name, 'name', startsWith('A'))); - Package Checks:所有
Subject上都直接内置了.has(feature, description)扩展方法,不仅少传一个参数,还能返回代表该属性的新Subject,以便直接在其后继续链式断言:check(actual).isA<Person>().has((p) => p.name, 'name').startsWith('A');
5. 同步 vs 异步的 throws
- 旧版 Matcher:在
package:matcher中,无论是同步闭包还是异步 Future,包裹在expect或expectLater里的throwsA行为表现基本一致。 - Package Checks:根据目标被测对象是同步还是异步,
.throws<E>()断言的行为和返回类型完全不同:- 同步 (
Subject<T Function()>):.throws<E>()会同步返回一个Subject<E>。它不接受回调函数作为参数!你需要直接在该返回的Subject<E>上继续接链式或级联断言:// 正确写法(同步链式/级联调用) check(() => triggerSyncError()).throws<ArgumentError>() ..has((e) => e.message, 'message').equals('invalid input'); // 错误写法(向同步 throws 传递回调会导致编译报错!) check(() => triggerSync").throws<ArgumentError>((it) => ...); // ERROR! - 异步 (
Subject<Future<T>>):.throws<E>()返回的是Future<void>。由于无法直接在Future<void>上继续链式调用,它必须传入一个检查回调函数:// 正确写法(异步回调) await check(triggerAsyncError()).throws<ArgumentError>((it) => it ..has((e) => e.message, 'message').equals('invalid input')); - 致命踩坑点:如果尝试在 awaited 后的异步
.throws<E>()后面直接接链式断言(例如await check(future).throws<E>().equals(...)),由于其返回类型为Future<void>,会导致无法通过编译。
- 同步 (
6. RegExp / Pattern 等值检查
- 旧版 Matcher:在
package:matcher中,expect(myPattern, equals(RegExp('Hello')))可以正常工作,因为旧版 matcher 的比较规则对 RegExp 实例做过特殊兼容处理。 - Package Checks:
.equals()使用 Dart 严格的==检查。因为不同的RegExp实例默认不满足==,直接调用.equals()会在运行时失败。 - 修复方案:结合使用
.isA<RegExp>()类型细化与级联符号(..),显式断言RegExp对象的属性:check(myPattern).isA<RegExp>() ..has((r) => r.pattern, 'pattern').equals('Hello') ..has((r) => r.isMultiLine, 'isMultiLine').isTrue();
7. 可空布尔值的类型安全限制(bool? 字段)
- 旧版 Matcher:静态类型层面,
isTrue和isFalse在运行时做松散的动态检查,隐式接受了可空布尔值(bool?)。 - Package Checks:
.isTrue()和.isFalse()严格定义在非空Subject<bool>上。在Subject<bool?>上无法直接调用。 - 修复方案:对于声明为
bool?的字段,要么先细化类型(如.isNotNull().isTrue()),要么直接使用通用的.equals(true)与.equals(false)(支持所有类型):// 如果 options.flagOutdated 是 bool? 类型 check(options.flagOutdated).equals(true); check(options.flagOutdated).equals(false);
8. Map Key 包含性断言(containsKey vs contains)
- 旧版 Matcher:在
package:matcher中,使用contains(key)来断言Map是否包含某个特定的 Key。 - Package Checks:在
Subject<Map>上直接调用.contains(...)是未定义的,会导致编译失败。 - 修复方案:改用 Map 专属的
.containsKey(key)断言:// 迁移前 (Matcher) expect(myMap, contains('my_key')); // 迁移后 (Checks) check(myMap).containsKey('my_key');
9. Extension Types 需显式指定泛型参数
- 旧版 Matcher:由于松散的动态类型比较,
expect(extensionTypeConst, 3)可以直接通过编译。 - Package Checks:假设
QrEciValue是基于int的 Extension Type(如extension type const QrEciValue(int value) implements int),在Subject<QrEciValue>上直接调用.equals(3)会失败,因为3(int类型)不能直接赋值给QrEciValue。而如果使用as int强转,又会触发静态分析警告 "Unnecessary cast",因为QrEciValue在静态类型上已经实现了int。 - 修复方案:在
check函数上显式指定泛型类型参数,强制 checks 将其作为基础类型处理:// 正确(类型安全且无分析警告) check<int>(QrEciValue.iso8859_1).equals(3);
10. 动态 Map / JSON 查找结果的类型转换
- 旧版 Matcher:松散的动态类型允许将静态类型为
dynamic的嵌套 JSON 查找结果直接与 List 或 Map 进行比较。 - Package Checks:严格的类型安全会拒绝在
.deepEquals(...)中将dynamic隐式赋值给Iterable<Object?>。 - 修复方案:将动态查找的结果显式转换为
List或Map:// 正确(显式强转为 List) check(myIterable).deepEquals(json['data']['items'] as List);
Matcher 到 Checks 映射表
测试断言快速速查替换表:
| 旧版 Matcher | Package Checks 等效写法 | 备注 |
|---|---|---|
expect(actual, expected) |
check(actual).equals(expected) |
集合类型请改用 .deepEquals! |
expect(actual, equals(expected)) |
check(actual).equals(expected) |
集合类型请改用 .deepEquals! |
isA<T>() |
check(actual).isA<T>() |
支持直接链式调用 |
same(expected) |
check(actual).identicalTo(expected) |
校验引用同一性 |
anyElement(matcher) |
check(iterable).any(conditionCallback) |
例如 check(list).any((e) => e.equals(1)) |
everyElement(matcher) |
check(iterable).every(conditionCallback) |
例如 check(list).every((e) => e.isGreaterThan(0)) |
hasLength(expected) |
check(actual).length.equals(expected) |
适用于 String、Map、Iterable 等 |
isNot(matcher) |
check(actual).not(conditionCallback) |
例如 check(val).not((it) => it.equals(5)) |
contains(element) |
check(actual).contains(element) |
适用于 String、Iterable(Map 请改用 containsKey) |






