其他
Dart 测试从 package:matcher 迁移到…
当 Dart/Flutter 测试套件使用 package:test 默认导出的 package:matcher 旧式 expect/expectLater,并希望迁移到 package:checks 的 check 断言时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
Dart 测试从 package:mat特定
解决思路
- 在 pubspec.yaml 中将 package:checks 添加为 dev_dependency:运行 `dart pub add dev:checks`。
- 如果 dev_dependencies 中显式列有 package:matcher,则移除它;通常 package:test 会传递引入 package:matcher,可保留。
- 用 `grep -rn "expect(" test/` 和 `grep -rn "expectLater(" test/` 定位所有含旧 expect 或 expectLater 的测试文件。
- 用 `grep -rn "expect(.*, \[" test/` 和 `grep -rn "expect(.*, {" test/` 查找集合相等陷阱。
- 用 `grep -rn "matches(" test/` 查找需要转为 RegExp + matchesPattern 的调用。
- 用 `grep -rn "having(" test/` 查找需要转为 .has() 的旧 TypeMatcher.having() 调用。
- 决定对每个目标文件进行完整迁移还是增量迁移。
- 更新 import:将 `import 'package:test/test.dart';` 替换为 `import 'package:test/scaffolding.dart';` 和 `import 'package:checks/checks.dart';`。
- 增量迁移时额外加 `import 'package:test/expect.dart';`,临时允许旧 expect()。
- 按 Matcher-to-Checks 映射表将旧 expect 和 expectLater 调用改写为 check 语法。
- 完整迁移完成后移除 `import 'package:test/expect.dart';`,让残留的未迁移 expect 调用立即变成编译错误以便定位和修复。
- 运行 `dart analyze`;重点检查 `.isA<Type>()` 的泛型参数,并确保异步 expectation 已正确 await,留意 `unawaited_futures` 警告。
- 运行 `dart test`;若测试失败,阅读 package:checks 的详细失败输出,判断是真实测试失败还是迁移时断言翻译错误。
- 集合相等陷阱:旧 `expect(actual, expected)` 或 `expect(actual, equals(expected))` 对 List/Map/Set 做深比较;新 `.equals(expected)` 等价于 Dart `==`,对集合会按身份比较并几乎必然失败;集合必须用 `.deepEquals(expected)`,如 `check(myList).deepEquals([1, 2, 3]);`。
- reason 参数改 because:旧 `expect(actual, expectation, reason: 'Explanation')`;新 `check(because: 'Explanation', actual).expectation();`。
- 正则匹配:旧 `matches(pattern)` 对 String 参数自动转 RegExp;新 `.matchesPattern(pattern)` 把 String 当字面量;要用正则需显式传 `RegExp`,如 `check(someString).matchesPattern(RegExp(r'\d+'));`。
- 属性提取:旧 `isA<Person>().having((p) => p.name, 'name', startsWith('A'))`;新 `check(actual).isA<Person>().has((p) => p.name, 'name').startsWith('A');`。
- 同步 throws:`check(() => triggerSyncError()).throws<ArgumentError>()..has((e) => e.message, 'message').equals('invalid input');`;不要把回调传给同步 `.throws<E>()`,否则编译错误。
- 异步 throws:`await check(triggerAsyncError()).throws<ArgumentError>((it) => it..has((e) => e.message, 'message').equals('invalid input'));`;异步 `.throws<E>()` 返回 `Future<void>`,必须用检查回调,不能直接链式 `.equals(...)`。
- RegExp/Pattern 相等:不要用 `.equals(RegExp('Hello'))`;改用 `.isA<RegExp>()` 加 cascade 断言属性,如 `check(myPattern).isA<RegExp>()..has((r) => r.pattern, 'pattern').equals('Hello')..has((r) => r.isMultiLine, 'isMultiLine').isTrue();`。
- 可空 bool 安全:`.isTrue()` 和 `.isFalse()` 只定义在 `Subject<bool>`,不适用于 `Subject<bool?>`;对 `bool?` 用 `.isNotNull().isTrue()` 或 `.equals(true)` / `.equals(false)`,如 `check(options.flagOutdated).equals(true);`。
- Map key containment:旧 `expect(myMap, contains('my_key'))`;新 `check(myMap).containsKey('my_key')`;不要对 `Subject<Map>` 调用 `.contains(...)`。
- extension type 泛型:对 extension type(如 `QrEciValue` 实现 `int`),不要用 `as int`(会触发 unnecessary cast 警告),改用 `check<int>(QrEciValue.iso8859_1).equals(3);` 显式指定泛型。
- dynamic Map/JSON 查找:对 `dynamic` 结果先显式 cast 再 `.deepEquals(...)`,如 `check(myIterable).deepEquals(json['data']['items'] as List);`。
- 映射:`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`。
- 映射:Map 上的 `contains(key)` -> `check(map).containsKey(key)`。
- 映射:`startsWith(prefix)` -> `check(string).startsWith(prefix)`。
- 映射:`endsWith(suffix)` -> `check(string).endsWith(suffix)`。
- 映射:`isEmpty` -> `check(actual).isEmpty()`,适用于 String、Map、Iterable。
- 映射:`isNotEmpty` -> `check(actual).isNotEmpty()`,适用于 String、Map、Iterable。
- 映射:`isNull` -> `check(actual).isNull()`。
- 映射:`isNotNull` -> `check(actual).isNotNull()`。
- 映射:`isTrue` / `true` -> `check(actual).isTrue()`,仅非空 bool。
- 映射:`isFalse` / `false` -> `check(actual).isFalse()`,仅非空 bool。
- 映射:`completion(matcher)` -> `await check(future).completes(conditionCallback)`,必须 await。
- 映射:`throwsA(matcher)` -> `await check(future).throws<Type>()`,必须 await。
- 映射:`emits(value)` -> `await check(streamQueue).emits(conditionCallback)`,必须 await。
- 映射:`emitsThrough(value)` -> `await check(streamQueue).emitsThrough(conditionCallback)`,必须 await。
- 映射:`stringContainsInOrder(list)` -> `check(string).containsInOrder(list)`。
- 映射:`pairwiseCompare(...)` -> `check(actual).pairwiseMatches(...)`。
- 无直接替换:`throwsArgumentError`、`throwsStateError`、`throwsUnsupportedError` 等特定错误匹配器,改用 `.throws<T>()` 指定错误类型,如 `await check(triggerError()).throws<ArgumentError>();`。
- 无直接替换:`anything` 匹配器,传空条件回调 `(_) {}`,如 `await check(someFuture).completes((_) {});`。
- 无直接替换:数值开关 `isPositive`、`isNegative`、`isZero`、`isNonPositive`、`isNonNegative`、`isNonZero`,改用显式比较:`isPositive` -> `isGreaterThan(0)`,`isNegative` -> `isLessThan(0)`,`isZero` -> `equals(0)`,`isNonNegative` -> `isGreaterOrEqual(0)`。
- 无直接替换:数值范围 `inClosedOpenRange(min, max)`、`inInclusiveRange(min, max)` 等,用 cascade 链式写边界:`check(actualValue)..isGreaterOrEqual(min)..isLessThan(max);`。
- 自定义 expectation 需要先 `import 'package:checks/context.dart';`,并在 `Subject<T>` 上写 extension 方法。
- 简单自定义 expectation:用 `context.expect` 检查属性并在失败时返回 `Rejection`,例如 `extension CustomPersonChecks on Subject<Person> { void isAdult() { context.expect(() => ['is an adult (age >= 18)'], (actual) { if (actual.age >= 18) return null; return Rejection(which: ['is only ${actual.age} years old']); }); } }`。
- 嵌套属性提取:简单且不会失败的字段用 `has`,如 `Subject<Address> get address => has((p) => p.address, 'address');`。
- 嵌套属性提取:可能失败或需要拒绝的字段用 `context.nest`,如 `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); });`。
- 异步自定义 expectation:使用 `context.expectAsync` 或 `context.nestAsync` 并返回 `Future`,例如 `extension CustomFutureChecks<T> on Subject<Future<T>> { Future<void> completesNormally() { return context.expectAsync(() => ['completes without throwing'], (actual) async { try { await actual; return null; } catch (e) { return Rejection(which: ['threw $e']); } }); } }`。
- 示例:基础断言:`expect(someValue, isNotNull);` -> `check(someValue).isNotNull();`;`expect(result, isTrue, reason: 'should be successful');` -> `check(because: 'should be successful', result).isTrue();`;`expect(myString, startsWith('hello'));` -> `check(myString).startsWith('hello');`。
- 示例:链式与 cascade:`expect(someString, allOf([startsWith('a'), contains('b'), endsWith('c')]));` -> `check(someString)..startsWith('a')..contains('b')..endsWith('c');`。
- 示例:异步 Stream:`var queue = StreamQueue(Stream.fromIterable([1, 2, 3])); await expectLater(queue, emitsInOrder([1, 2, 3]));` -> `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))]);`。
适用边界 · 注意事项
- 非 Dart 项目,或测试未使用 package:matcher 的 expect/expectLater 时。
- 尚未决定引入 package:checks 依赖,或需要保持不新增 dev_dependency 的项目。
- 目标断言在 package:checks 中没有等价物,且不打算通过 context 编写自定义 extension 时。