dart-migrate-to-checks-package

dart-migrate-to-checks-package

热门

将 Dart 测试代码中来自旧版 `package:matcher` 的 `expect` 及相关断言函数,重构迁移为现代、类型安全的 `package:checks` 等效写法。

2792Star
164Fork
更新于 2026/8/5
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.yamldev_dependencies 中添加 package:checks
    dart pub add dev:checks
    
  • 如果 pubspec.yamldev_dependencies 中显式声明了 package:matcher,可直接移除(它通常会被 package:test 传递依赖进来,无需显式声明)。

2. 梳理与规划目标文件

  • 使用 查找与定位策略 章节中的 grep 正则模式,找出所有包含旧版 expectexpectLater 调用的测试文件。
  • 根据项目情况,评估选择一次性全量迁移还是按文件/用例增量迁移。

3. 迁移单个测试文件(增量或全量)

针对任意目标测试文件:

  1. 更新 Import 导入语句
    • 将原有的 import 'package:test/test.dart'; 替换为:
      import 'package:test/scaffolding.dart';
      import 'package:checks/checks.dart';
      
    • 增量迁移场景:如果你只想迁移文件中的部分测试用例,或希望分步平滑迁移,可暂时保留或添加:
      import 'package:test/expect.dart'; // 临时允许继续使用旧版 expect()
      
  2. 改写断言语句:参照 核心语法差异与避坑指南Matcher 到 Checks 映射表,将旧版 expectexpectLater 重构为 check 链式语法。
  3. 利用编译器验证:若是全量迁移,直接删掉 import 'package:test/expect.dart'; 这一行。此时文件中任何残留的旧版 expect 都会立即报编译错误,方便快速排查定位。

4. 验证与反馈循环

  • 静态代码分析:在目标 Package 根目录运行 Dart 静态分析:
    dart analyze
    
    重点检查 .isA<Type>() 上的泛型类型参数是否匹配,并确认所有异步断言(Expectations)都已加上 await(关注是否有 unawaited_futures 分析警告)。
  • 运行测试套件:执行测试,校验功能逻辑与断言运行时逻辑是否均无误:
    dart test
    
    若有测试用例失败,仔细分析 package: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

  • 旧版 Matchermatches(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,包裹在 expectexpectLater 里的 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:静态类型层面,isTrueisFalse 在运行时做松散的动态检查,隐式接受了可空布尔值(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) 会失败,因为 3int 类型)不能直接赋值给 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?>
  • 修复方案:将动态查找的结果显式转换为 ListMap
    // 正确(显式强转为 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