dart-migrate-to-checks-package

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Migrating Dart Tests to Package Checks

将 Dart 测试迁移至 Package Checks

Use this skill when you need to migrate a Dart test suite from the legacy
package:matcher
(which is exported by default from
package:test/test.dart
) to the modern, type-safe, and literate
package:checks
assertion library.
当你需要将 Dart 测试套件从传统的
package:matcher
(默认从
package:test/test.dart
导出)迁移至现代、类型安全且可读性强的
package:checks
断言库时,可以使用此技能。

Contents

目录

When to Use This Skill

何时使用此技能

  • When asked to "migrate tests to checks", "use package:checks", or "modernize test assertions".
  • When updating legacy test suites where static type safety, better autocomplete in IDEs, and highly detailed failure diagnostics are desired.

  • 当被要求“将测试迁移至 checks”、“使用 package:checks”或“现代化测试断言”时。
  • 当更新传统测试套件,需要静态类型安全、IDE 中更好的自动补全以及更详细的失败诊断时。

How to Use This Skill (The Workflow)

如何使用此技能(工作流程)

Follow this structured workflow to safely and systematically migrate a test suite:
遵循以下结构化工作流程,安全且系统地迁移测试套件:

1. Dependency Setup

1. 依赖配置

  • Add
    package:checks
    as a
    dev_dependency
    in
    pubspec.yaml
    :
    bash
    dart pub add dev:checks
  • Remove
    package:matcher
    if it is explicitly listed under
    dev_dependencies
    (it is typically transitively included by
    package:test
    , which is fine).
  • pubspec.yaml
    中将
    package:checks
    添加为
    dev_dependency
    bash
    dart pub add dev:checks
  • 如果
    package:matcher
    被显式列在
    dev_dependencies
    下,请将其移除(它通常由
    package:test
    间接引入,这种情况无需处理)。

2. Identify and Plan Target Files

2. 识别并规划目标文件

  • Use the grep patterns in Strategies for Discovery to locate all test files containing legacy
    expect
    or
    expectLater
    calls.
  • Decide whether to migrate files fully or incrementally.
  • 使用发现策略中的 grep 模式定位所有包含传统
    expect
    expectLater
    调用的测试文件。
  • 决定是完全迁移文件还是增量迁移。

3. Migrating a File (Incremental or Full)

3. 迁移文件(增量或完全)

For any target test file:
  1. Update Imports:
    • Replace the generic
      import 'package:test/test.dart';
      with:
      dart
      import 'package:test/scaffolding.dart';
      import 'package:checks/checks.dart';
    • For Incremental Migration: If you only want to migrate some test cases in the file, or want to migrate one step at a time, add:
      dart
      import 'package:test/expect.dart'; // Temporarily allows legacy expect()
  2. Translate Assertions: Rewrite legacy
    expect
    and
    expectLater
    calls to
    check
    syntax following the Key Syntax Differences and Pitfalls and the Matcher-to-Checks Mapping Table.
  3. Verify via Compiler: If migrating fully, remove the
    import 'package:test/expect.dart';
    line. Any remaining un-migrated
    expect
    calls will immediately surface as compiler errors, making them easy to find and fix.
对于任意目标测试文件:
  1. 更新导入
    • 将通用的
      import 'package:test/test.dart';
      替换为:
      dart
      import 'package:test/scaffolding.dart';
      import 'package:checks/checks.dart';
    • 增量迁移:如果你只想迁移文件中的部分测试用例,或者想分步迁移,请添加:
      dart
      import 'package:test/expect.dart'; // 临时允许使用传统 expect()
  2. 转换断言:遵循关键语法差异与陷阱Matcher 到 Checks 的映射表,将传统的
    expect
    expectLater
    调用重写为
    check
    语法。
  3. 通过编译器验证:如果是完全迁移,请移除
    import 'package:test/expect.dart';
    行。任何未迁移的
    expect
    调用都会立即触发编译器错误,便于查找和修复。

4. Verification and Feedback Loops

4. 验证与反馈循环

  • Static Analysis: Run static analysis on the target package:
    bash
    dart analyze
    Pay close attention to generic type parameters on
    .isA<Type>()
    and ensure asynchronous expectations are properly awaited (check for
    unawaited_futures
    warnings).
  • Run Tests: Execute the tests to verify both behavior and correct assertion runtime logic:
    bash
    dart test
    If a test fails, review the extremely detailed failure output of
    package:checks
    to diagnose if the test is genuinely failing or if the expectation was translated incorrectly.

  • 静态分析:对目标包运行静态分析:
    bash
    dart analyze
    特别注意
    .isA<Type>()
    上的泛型类型参数,并确保异步断言已正确等待(检查
    unawaited_futures
    警告)。
  • 运行测试:执行测试以验证行为和断言运行时逻辑是否正确:
    bash
    dart test
    如果测试失败,请查看
    package:checks
    极其详细的失败输出,以诊断测试是真的失败还是期望转换错误。

Key Syntax Differences and Pitfalls

关键语法差异与陷阱

[!IMPORTANT] A line-for-line translation can sometimes introduce subtle bugs or false passes. Always review these key differences carefully:
[!IMPORTANT] 逐行转换有时会引入细微的 bug 或误判。请务必仔细查看以下关键差异:

1. Collection Equality Pitfall (
equals
vs
deepEquals
)

1. 集合相等陷阱(
equals
vs
deepEquals

  • Legacy Matcher:
    expect(actual, expected)
    or
    expect(actual, equals(expected))
    performed a deep equality check if the arguments were collections (Lists, Maps, Sets).
  • Package Checks:
    .equals(expected)
    corresponds strictly to
    operator ==
    . Since Dart collections do not override
    operator ==
    for element-wise comparison, using
    .equals
    on a collection will check for identity and almost certainly fail at runtime.
  • Remediation: You must replace collection equality assertions with
    .deepEquals(expected)
    .
    dart
    // BEFORE (Matcher)
    expect(myList, [1, 2, 3]);
    
    // AFTER (Checks)
    check(myList).deepEquals([1, 2, 3]);
  • 传统 Matcher
    expect(actual, expected)
    expect(actual, equals(expected))
    在参数为集合(列表、映射、集合)时会执行深度相等检查
  • Package Checks
    .equals(expected)
    严格对应
    operator ==
    。由于 Dart 集合未针对元素级比较重写
    operator ==
    ,因此对集合使用
    .equals
    会检查同一性,几乎肯定会在运行时失败。
  • 修复方案:必须将集合相等断言替换为
    .deepEquals(expected)
    dart
    // 之前(Matcher)
    expect(myList, [1, 2, 3]);
    
    // 之后(Checks)
    check(myList).deepEquals([1, 2, 3]);

2. The
reason
Parameter is now
because

2.
reason
参数变为
because

  • Legacy Matcher: The explanation was passed as a trailing named argument
    reason
    to
    expect
    :
    dart
    expect(actual, expectation, reason: 'Explanation');
  • Package Checks: The explanation is passed as the named argument
    because
    to the
    check
    function before the actual subject:
    dart
    check(because: 'Explanation', actual).expectation();
  • 传统 Matcher:解释作为尾随命名参数
    reason
    传递给
    expect
    dart
    expect(actual, expectation, reason: 'Explanation');
  • Package Checks:解释作为命名参数
    because
    传递给
    check
    函数,且位于实际检查对象之前:
    dart
    check(because: 'Explanation', actual).expectation();

3. Regular Expression Matching (
matches
vs
matchesPattern
)

3. 正则表达式匹配(
matches
vs
matchesPattern

  • Legacy Matcher: The
    matches(pattern)
    matcher automatically converted a
    String
    argument into a
    RegExp
    (e.g.,
    matches(r'\d')
    matched
    '1'
    ).
  • Package Checks:
    .matchesPattern(pattern)
    treats a
    String
    argument as a literal string pattern.
  • Remediation: To match using a regular expression, you must explicitly pass a
    RegExp
    object:
    dart
    // BEFORE (Matcher)
    expect(someString, matches(r'\d+'));
    
    // AFTER (Checks)
    check(someString).matchesPattern(RegExp(r'\d+'));
  • 传统 Matcher
    matches(pattern)
    匹配器会自动将
    String
    参数转换为
    RegExp
    (例如,
    matches(r'\d')
    匹配
    '1'
    )。
  • Package Checks
    .matchesPattern(pattern)
    String
    参数视为字面字符串模式。
  • 修复方案:要使用正则表达式进行匹配,必须显式传递
    RegExp
    对象:
    dart
    // 之前(Matcher)
    expect(someString, matches(r'\d+'));
    
    // 之后(Checks)
    check(someString).matchesPattern(RegExp(r'\d+'));

4. Property Extraction (
TypeMatcher.having
vs
.has
)

4. 属性提取(
TypeMatcher.having
vs
.has

  • Legacy Matcher: Chained field/property expectations used
    TypeMatcher.having(feature, description, matcher)
    :
    dart
    expect(actual, isA<Person>().having((p) => p.name, 'name', startsWith('A')));
  • Package Checks: The
    .has(feature, description)
    extension is available on all
    Subject
    s, takes one fewer argument, and returns a new
    Subject
    representing that property. You chain expectations directly off it:
    dart
    check(actual).isA<Person>().has((p) => p.name, 'name').startsWith('A');
  • 传统 Matcher:链式字段/属性期望使用
    TypeMatcher.having(feature, description, matcher)
    dart
    expect(actual, isA<Person>().having((p) => p.name, 'name', startsWith('A')));
  • Package Checks
    .has(feature, description)
    扩展方法适用于所有
    Subject
    ,少一个参数,并返回表示该属性的新
    Subject
    。你可以直接在其上链式调用期望:
    dart
    check(actual).isA<Person>().has((p) => p.name, 'name').startsWith('A');

5. Synchronous vs. Asynchronous
throws

5. 同步与异步
throws

  • Legacy Matcher: In
    package:matcher
    ,
    throwsA
    behaved similarly for both synchronous closures and asynchronous futures when wrapped in
    expect
    or
    expectLater
    .
  • Package Checks: The
    .throws<E>()
    expectation behaves differently and has different return types depending on whether the subject is synchronous or asynchronous:
    • Synchronous (
      Subject<T Function()>
      ):
      .throws<E>()
      returns a
      Subject<E>
      synchronously. This does not accept a callback argument! You chain or cascade expectations directly off the returned
      Subject<E>
      :
      dart
      // YES (Synchronous chaining)
      check(() => triggerSyncError()).throws<ArgumentError>()
        ..has((e) => e.message, 'message').equals('invalid input');
      
      // NO (Passing a callback to sync throws will cause a compiler error!)
      check(() => triggerSync").throws<ArgumentError>((it) => ...); // ERROR!
    • Asynchronous (
      Subject<Future<T>>
      ):
      .throws<E>()
      returns
      Future<void>
      . Because you cannot chain directly off a
      Future<void>
      , this requires an inspection callback:
      dart
      // YES (Asynchronous callback)
      await check(triggerAsyncError()).throws<ArgumentError>((it) => it
        ..has((e) => e.message, 'message').equals('invalid input'));
    • Crucial Pitfall: Trying to chain expectations directly after an awaited asynchronous
      .throws<E>()
      (e.g.,
      await check(future).throws<E>().equals(...)
      ) will fail to compile because it returns
      Future<void>
      .
  • 传统 Matcher:在
    package:matcher
    中,
    throwsA
    对同步闭包和异步 future 的行为在
    expect
    expectLater
    中类似。
  • Package Checks
    .throws<E>()
    期望的行为和返回类型取决于检查对象是同步还是异步:
    • 同步
      Subject<T Function()>
      ):
      .throws<E>()
      同步返回
      Subject<E>
      。这接受回调参数!你可以直接在返回的
      Subject<E>
      上链式或级联调用期望:
      dart
      // 正确(同步链式调用)
      check(() => triggerSyncError()).throws<ArgumentError>()
        ..has((e) => e.message, 'message').equals('invalid input');
      
      // 错误(向同步 throws 传递回调会导致编译器错误!)
      check(() => triggerSyncError()).throws<ArgumentError>((it) => ...); // ERROR!
    • 异步
      Subject<Future<T>>
      ):
      .throws<E>()
      返回
      Future<void>
      。由于无法直接在
      Future<void>
      上链式调用,因此需要检查回调:
      dart
      // 正确(异步回调)
      await check(triggerAsyncError()).throws<ArgumentError>((it) => it
        ..has((e) => e.message, 'message').equals('invalid input'));
    • 关键陷阱:在等待异步
      .throws<E>()
      后直接链式调用期望(例如,
      await check(future).throws<E>().equals(...)
      )会编译失败,因为它返回
      Future<void>

6. RegExp / Pattern Equality

6. RegExp / Pattern 相等性

  • Legacy Matcher: In
    package:matcher
    ,
    expect(myPattern,
    equals(RegExp('Hello')))
    worked because the matcher comparison rules handled RegExp instances.
  • Package Checks:
    .equals()
    uses strict Dart
    ==
    equality. Since separate
    RegExp
    instances do not satisfy
    ==
    , using
    .equals()
    will fail at runtime.
  • Remediation: Use
    .isA<RegExp>()
    type refinement along with cascades to assert on the properties of the
    RegExp
    object explicitly:
    dart
    check(myPattern).isA<RegExp>()
      ..has((r) => r.pattern, 'pattern').equals('Hello')
      ..has((r) => r.isMultiLine, 'isMultiLine').isTrue();
  • 传统 Matcher:在
    package:matcher
    中,
    expect(myPattern, equals(RegExp('Hello')))
    可以正常工作,因为匹配器比较规则处理了 RegExp 实例。
  • Package Checks
    .equals()
    使用严格的 Dart
    ==
    相等性。由于不同的
    RegExp
    实例不满足
    ==
    ,使用
    .equals()
    会在运行时失败。
  • 修复方案:使用
    .isA<RegExp>()
    类型细化,并结合级联操作显式断言
    RegExp
    对象的属性:
    dart
    check(myPattern).isA<RegExp>()
      ..has((r) => r.pattern, 'pattern').equals('Hello')
      ..has((r) => r.isMultiLine, 'isMultiLine').isTrue();

7. Strict Nullable Boolean Safety (
bool?
fields)

7. 严格可空布尔安全性(
bool?
字段)

  • Legacy Matcher: Statically,
    isTrue
    and
    isFalse
    performed loose dynamic checks at runtime, which silently accepted nullable booleans (
    bool?
    ).
  • Package Checks:
    .isTrue()
    and
    .isFalse()
    are defined strictly on
    Subject<bool>
    (non-nullable). They are not available on
    Subject<bool?>
    .
  • Remediation: For fields declared as
    bool?
    , you must either refine the subject (e.g.,
    .isNotNull().isTrue()
    ) or simply use
    .equals(true)
    and
    .equals(false)
    which are generic and work on all types:
    dart
    // If options.flagOutdated is a bool?
    check(options.flagOutdated).equals(true);
    check(options.flagOutdated).equals(false);
  • 传统 Matcher:在静态层面,
    isTrue
    isFalse
    在运行时执行松散的动态检查,会静默接受可空布尔值(
    bool?
    )。
  • Package Checks
    .isTrue()
    .isFalse()
    严格定义在
    Subject<bool>
    (非可空)上。它们不适用于
    Subject<bool?>
  • 修复方案:对于声明为
    bool?
    的字段,你必须细化检查对象(例如,
    .isNotNull().isTrue()
    ),或者直接使用
    .equals(true)
    .equals(false)
    ,它们是泛型的,适用于所有类型:
    dart
    // 如果 options.flagOutdated 是 bool?
    check(options.flagOutdated).equals(true);
    check(options.flagOutdated).equals(false);

8. Map Key Containment (
containsKey
vs
contains
)

8. 映射键包含(
containsKey
vs
contains

  • Legacy Matcher: In
    package:matcher
    ,
    contains(key)
    was used to assert that a
    Map
    contained a specific key.
  • Package Checks: Calling
    .contains(...)
    on a
    Subject<Map>
    is not defined and will fail compilation.
  • Remediation: Use the map-specific
    .containsKey(key)
    matcher instead:
    dart
    // BEFORE (Matcher)
    expect(myMap, contains('my_key'));
    
    // AFTER (Checks)
    check(myMap).containsKey('my_key');
  • 传统 Matcher:在
    package:matcher
    中,
    contains(key)
    用于断言
    Map
    包含特定键。
  • Package Checks:对
    Subject<Map>
    调用
    .contains(...)
    未定义,会编译失败。
  • 修复方案:改用映射专用的
    .containsKey(key)
    匹配器:
    dart
    // 之前(Matcher)
    expect(myMap, contains('my_key'));
    
    // 之后(Checks)
    check(myMap).containsKey('my_key');

9. Explicit Generic Parameters for Extension Types

9. 扩展类型的显式泛型参数

  • Legacy Matcher:
    expect(extensionTypeConst, 3)
    compiled because of loose dynamic equality.
  • Package Checks: If
    QrEciValue
    is an extension type representation of
    int
    (e.g.,
    extension type const QrEciValue(int value) implements int
    ), calling
    .equals(3)
    on a
    Subject<QrEciValue>
    fails because
    3
    (an
    int
    ) is not assignable to
    QrEciValue
    . Casting with
    as int
    will trigger an "Unnecessary cast" static analysis warning because
    QrEciValue
    statically implements
    int
    .
  • Remediation: Explicitly specify the generic type parameter on the
    check
    function to force checks to treat it as the primitive type:
    dart
    // YES (Type-safe and warning-free)
    check<int>(QrEciValue.iso8859_1).equals(3);
  • 传统 Matcher
    expect(extensionTypeConst, 3)
    可以编译,因为松散的动态相等性。
  • Package Checks:如果
    QrEciValue
    int
    的扩展类型表示(例如,
    extension type const QrEciValue(int value) implements int
    ),对
    Subject<QrEciValue>
    调用
    .equals(3)
    会失败,因为
    3
    int
    类型)无法赋值给
    QrEciValue
    。使用
    as int
    进行强制转换会触发“不必要的强制转换”静态分析警告,因为
    QrEciValue
    在静态层面实现了
    int
  • 修复方案:在
    check
    函数上显式指定泛型类型参数,强制检查将其视为原始类型:
    dart
    // 正确(类型安全且无警告)
    check<int>(QrEciValue.iso8859_1).equals(3);

10. Dynamic Map / JSON Lookup Casting

10. 动态映射 / JSON 查找强制转换

  • Legacy Matcher: Loose dynamic typing allowed comparing nested json lookups statically typed as
    dynamic
    directly against lists or maps.
  • Package Checks: Strict type safety rejects the implicit assignment of
    dynamic
    to
    Iterable<Object?>
    in
    .deepEquals(...)
    .
  • Remediation: Statically cast the dynamic lookup result to a
    List
    or
    Map
    :
    dart
    // YES (Explicit cast to List)
    check(myIterable).deepEquals(json['data']['items'] as List);

  • 传统 Matcher:松散的动态类型允许将静态类型为
    dynamic
    的嵌套 JSON 查找结果直接与列表或映射比较。
  • Package Checks:严格的类型安全性拒绝将
    dynamic
    隐式赋值给
    .deepEquals(...)
    中的
    Iterable<Object?>
  • 修复方案:将动态查找结果静态强制转换为
    List
    Map
    dart
    // 正确(显式强制转换为 List)
    check(myIterable).deepEquals(json['data']['items'] as List);

Matcher-to-Checks Mapping Table

Matcher 到 Checks 的映射表

Use this table as a quick reference for direct matcher replacements:
Legacy MatcherPackage Checks EquivalentNotes
expect(actual, expected)
check(actual).equals(expected)
Use
.deepEquals
for collections!
expect(actual, equals(expected))
check(actual).equals(expected)
Use
.deepEquals
for collections!
isA<T>()
check(actual).isA<T>()
Chaining is supported directly
same(expected)
check(actual).identicalTo(expected)
Verifies identity
anyElement(matcher)
check(iterable).any(conditionCallback)
E.g.
check(list).any((e) => e.equals(1))
everyElement(matcher)
check(iterable).every(conditionCallback)
E.g.
check(list).every((e) => e.isGreaterThan(0))
hasLength(expected)
check(actual).length.equals(expected)
Works on String, Map, Iterable, etc.
isNot(matcher)
check(actual).not(conditionCallback)
E.g.
check(val).not((it) => it.equals(5))
contains(element)
check(actual).contains(element)
Works on String, Iterable (use
containsKey
for Map!)
contains(key)
(on a Map)
check(map).containsKey(key)
Map key containment
startsWith(prefix)
check(string).startsWith(prefix)
String only
endsWith(suffix)
check(string).endsWith(suffix)
String only
isEmpty
check(actual).isEmpty()
Works on String, Map, Iterable
isNotEmpty
check(actual).isNotEmpty()
Works on String, Map, Iterable
isNull
check(actual).isNull()
isNotNull
check(actual).isNotNull()
isTrue
/
true
check(actual).isTrue()
Works on non-nullable
bool
only
isFalse
/
false
check(actual).isFalse()
Works on non-nullable
bool
only
completion(matcher)
await check(future).completes(conditionCallback)
Must be awaited!
throwsA(matcher)
await check(future).throws<Type>()
Must be awaited!
emits(value)
await check(streamQueue).emits(conditionCallback)
Must be awaited!
emitsThrough(value)
await check(streamQueue).emitsThrough(conditionCallback)
Must be awaited!
stringContainsInOrder(list)
check(string).containsInOrder(list)
String only
pairwiseCompare(...)
check(actual).pairwiseMatches(...)

使用此表快速查找直接的 matcher 替代方案:
传统 MatcherPackage 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)
适用于字符串、映射、可迭代对象等
isNot(matcher)
check(actual).not(conditionCallback)
示例:
check(val).not((it) => it.equals(5))
contains(element)
check(actual).contains(element)
适用于字符串、可迭代对象(映射请使用
containsKey
!)
contains(key)
(映射上)
check(map).containsKey(key)
映射键包含
startsWith(prefix)
check(string).startsWith(prefix)
仅适用于字符串
endsWith(suffix)
check(string).endsWith(suffix)
仅适用于字符串
isEmpty
check(actual).isEmpty()
适用于字符串、映射、可迭代对象
isNotEmpty
check(actual).isNotEmpty()
适用于字符串、映射、可迭代对象
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)
必须等待!
throwsA(matcher)
await check(future).throws<Type>()
必须等待!
emits(value)
await check(streamQueue).emits(conditionCallback)
必须等待!
emitsThrough(value)
await check(streamQueue).emitsThrough(conditionCallback)
必须等待!
stringContainsInOrder(list)
check(string).containsInOrder(list)
仅适用于字符串
pairwiseCompare(...)
check(actual).pairwiseMatches(...)

Matchers with No Direct Replacements

无直接替代方案的 Matcher

Some legacy matchers do not have a one-to-one equivalent in
package:checks
due to API cleanup. Use these standard workarounds:
部分传统 matcher 在
package:checks
中没有一对一的等效方案,这是由于 API 精简导致的。请使用以下标准解决方法:

1. Specific Error Matchers

1. 特定错误匹配器

  • Legacy:
    throwsArgumentError
    ,
    throwsStateError
    ,
    throwsUnsupportedError
    , etc.
  • Checks: Use
    .throws<T>()
    with the specific error type:
    dart
    await check(triggerError()).throws<ArgumentError>();
  • 传统
    throwsArgumentError
    throwsStateError
    throwsUnsupportedError
    等。
  • Checks:使用
    .throws<T>()
    并指定特定错误类型:
    dart
    await check(triggerError()).throws<ArgumentError>();

2. The
anything
Matcher

2.
anything
匹配器

  • Legacy:
    expect(actual, anything)
  • Checks: Pass an empty condition callback
    (_) {}
    when a condition is syntactically required:
    dart
    await check(someFuture).completes((_) {});
  • 传统
    expect(actual, anything)
  • Checks:当语法上需要条件时,传递空条件回调
    (_) {}
    dart
    await check(someFuture).completes((_) {});

3. Specific Numeric Toggles

3. 特定数值判断

  • Legacy:
    isPositive
    ,
    isNegative
    ,
    isZero
    ,
    isNonPositive
    ,
    isNonNegative
    ,
    isNonZero
  • Checks: Use explicit comparative expectations:
    • isPositive
      $\rightarrow$
      isGreaterThan(0)
    • isNegative
      $\rightarrow$
      isLessThan(0)
    • isZero
      $\rightarrow$
      equals(0)
    • isNonNegative
      $\rightarrow$
      isGreaterOrEqual(0)
  • 传统
    isPositive
    isNegative
    isZero
    isNonPositive
    isNonNegative
    isNonZero
  • Checks:使用显式的比较期望:
    • isPositive
      isGreaterThan(0)
    • isNegative
      isLessThan(0)
    • isZero
      equals(0)
    • isNonNegative
      isGreaterOrEqual(0)

4. Numeric Ranges

4. 数值范围

  • Legacy:
    inClosedOpenRange(min, max)
    ,
    inInclusiveRange(min, max)
    , etc.
  • Checks: Chain the boundaries using the cascade operator (
    ..
    ):
    dart
    check(actualValue)
      ..isGreaterOrEqual(min)
      ..isLessThan(max);

  • 传统
    inClosedOpenRange(min, max)
    inInclusiveRange(min, max)
    等。
  • Checks:使用级联运算符(
    ..
    )链式调用边界条件:
    dart
    check(actualValue)
      ..isGreaterOrEqual(min)
      ..isLessThan(max);

Writing Custom Expectations (Replacing Custom Matchers)

编写自定义期望(替换自定义 Matcher)

When migrating from a legacy codebase, you may encounter custom
Matcher
subclasses. In
package:checks
, custom assertions are implemented as
extension
methods on
Subject<T>
.
To write custom expectations, you must import the checks context API:
dart
import 'package:checks/context.dart';
从传统代码库迁移时,你可能会遇到自定义
Matcher
子类。在
package:checks
中,自定义断言通过
Subject<T>
上的
extension
方法实现。
要编写自定义期望,必须导入 checks 上下文 API:
dart
import 'package:checks/context.dart';

1. Simple Custom Expectations (using
expect
)

1. 简单自定义期望(使用
expect

Use
context.expect
to check a property and return a
Rejection
on failure:
dart
extension CustomPersonChecks on Subject<Person> {
  void isAdult() {
    context.expect(
      () => ['is an adult (age >= 18)'],
      (actual) {
        if (actual.age >= 18) return null; // Pass
        return Rejection(
          which: ['is only ${actual.age} years old'],
        );
      },
    );
  }
}
使用
context.expect
检查属性,并在失败时返回
Rejection
dart
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'],
        );
      },
    );
  }
}

2. Nested Property Extraction (using
nest
or
has
)

2. 嵌套属性提取(使用
nest
has

To extract a property and allow further chained checks, use
nest
or the simpler
has
helper:
  • Using
    has
    (Recommended for simple, non-failing field access)
    :
    dart
    extension CustomPersonChecks on Subject<Person> {
      Subject<Address> get address => has((p) => p.address, 'address');
    }
  • Using
    nest
    (For property extraction that can fail or reject)
    :
    dart
    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);
        },
      );
    }
要提取属性并允许进一步的链式检查,请使用
nest
或更简单的
has
助手:
  • 使用
    has
    (推荐用于简单、不会失败的字段访问)
    dart
    extension CustomPersonChecks on Subject<Person> {
      Subject<Address> get address => has((p) => p.address, 'address');
    }
  • 使用
    nest
    (用于可能失败或拒绝的属性提取)
    dart
    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. Asynchronous Custom Expectations

3. 异步自定义期望

If the expectation is asynchronous (e.g. checking a Future or Stream), use
context.expectAsync
or
context.nestAsync
and return the resulting
Future
:
dart
extension CustomFutureChecks<T> on Subject<Future<T>> {
  Future<void> completesNormally() {
    return context.expectAsync(
      () => ['completes without throwing'],
      (actual) async {
        try {
          await actual;
          return null; // Pass
        } catch (e) {
          return Rejection(which: ['threw $e']);
        }
      },
    );
  }
}

如果期望是异步的(例如检查 Future 或 Stream),请使用
context.expectAsync
context.nestAsync
并返回结果
Future
dart
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']);
        }
      },
    );
  }
}

Strategies for Discovery

发现策略

Execute these commands in the terminal to identify legacy matchers and files requiring migration:
bash
undefined
在终端中执行以下命令,识别需要迁移的传统 matcher 和文件:
bash
undefined

1. Find all test files containing legacy expect() or expectLater()

1. 查找所有包含传统 expect() 或 expectLater() 的测试文件

grep -rn "expect(" test/ grep -rn "expectLater(" test/
grep -rn "expect(" test/ grep -rn "expectLater(" test/

2. Find potential collection equality pitfalls (literal lists or maps)

2. 查找潜在的集合相等陷阱(字面量列表或映射)

grep -rn "expect(., [" test/ grep -rn "expect(., {" test/
grep -rn "expect(., [" test/ grep -rn "expect(., {" test/

3. Find matches() calls (need conversion to RegExp + matchesPattern)

3. 查找 matches() 调用(需要转换为 RegExp + matchesPattern)

grep -rn "matches(" test/
grep -rn "matches(" test/

4. Find legacy TypeMatcher.having() calls (which need conversion to .has())

4. 查找传统 TypeMatcher.having() 调用(需要转换为 .has())

grep -rn "having(" test/

---
grep -rn "having(" test/

---

Examples

示例

Basic Assertions

基础断言

Before (Matcher):
dart
expect(someValue, isNotNull);
expect(result, isTrue, reason: 'should be successful');
expect(myString, startsWith('hello'));
After (Checks):
dart
check(someValue).isNotNull();
check(because: 'should be successful', result).isTrue();
check(myString).startsWith('hello');
之前(Matcher):
dart
expect(someValue, isNotNull);
expect(result, isTrue, reason: 'should be successful');
expect(myString, startsWith('hello'));
之后(Checks):
dart
check(someValue).isNotNull();
check(because: 'should be successful', result).isTrue();
check(myString).startsWith('hello');

Collection and Deep Equality

集合与深度相等

Before (Matcher):
dart
expect(items, [1, 2, 3]);
expect(configMap, equals({'port': 8080}));
After (Checks):
dart
check(items).deepEquals([1, 2, 3]);
check(configMap).deepEquals({'port': 8080});
之前(Matcher):
dart
expect(items, [1, 2, 3]);
expect(configMap, equals({'port': 8080}));
之后(Checks):
dart
check(items).deepEquals([1, 2, 3]);
check(configMap).deepEquals({'port': 8080});

Chaining and Cascades

链式调用与级联

Before (Matcher):
dart
expect(someString, allOf([
  startsWith('a'),
  contains('b'),
  endsWith('c'),
]));
After (Checks):
dart
check(someString)
  ..startsWith('a')
  ..contains('b')
  ..endsWith('c');
之前(Matcher):
dart
expect(someString, allOf([
  startsWith('a'),
  contains('b'),
  endsWith('c'),
]));
之后(Checks):
dart
check(someString)
  ..startsWith('a')
  ..contains('b')
  ..endsWith('c');

Complex Property Matching (has)

复杂属性匹配(has)

Before (Matcher):
dart
expect(response, isA<Response>()
    .having((r) => r.statusCode, 'statusCode', 200)
    .having((r) => r.body, 'body', contains('success')));
After (Checks):
dart
check(response).isA<Response>()
  ..has((r) => r.statusCode, 'statusCode').equals(200)
  ..has((r) => r.body, 'body').contains('success');
之前(Matcher):
dart
expect(response, isA<Response>()
    .having((r) => r.statusCode, 'statusCode', 200)
    .having((r) => r.body, 'body', contains('success')));
之后(Checks):
dart
check(response).isA<Response>()
  ..has((r) => r.statusCode, 'statusCode').equals(200)
  ..has((r) => r.body, 'body').contains('success');

Asynchronous Futures

异步 Future

Before (Matcher):
dart
expect(fetchData(), completes);
expect(fetchData(), completion(equals('data')));
expect(failingCall(), throwsA(isA<StateError>()));
After (Checks):
dart
await check(fetchData()).completes();
await check(fetchData()).completes((it) => it.equals('data'));
await check(failingCall()).throws<StateError>();
之前(Matcher):
dart
expect(fetchData(), completes);
expect(fetchData(), completion(equals('data')));
expect(failingCall(), throwsA(isA<StateError>()));
之后(Checks):
dart
await check(fetchData()).completes();
await check(fetchData()).completes((it) => it.equals('data'));
await check(failingCall()).throws<StateError>();

Asynchronous Streams

异步 Stream

Before (Matcher):
dart
var queue = StreamQueue(Stream.fromIterable([1, 2, 3]));
await expectLater(queue, emitsInOrder([1, 2, 3]));
After (Checks):
dart
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)),
]);
之前(Matcher):
dart
var queue = StreamQueue(Stream.fromIterable([1, 2, 3]));
await expectLater(queue, emitsInOrder([1, 2, 3]));
之后(Checks):
dart
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)),
]);