dart-migrate-to-checks-package

dart-migrate-to-checks-package

熱門

將使用 `package:matcher` 的 `expect` 及相關函式,重構替換為 `package:checks` 的對應寫法。

2792星標
164分支
更新於 2026/8/5
SKILL.md
唯讀
名稱
dart-migrate-to-checks-package
描述

將使用 `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 中新增 package:checksdev_dependency
    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. 驗證與回饋機制

  • 靜態分析:對目標套件執行靜態分析:
    dart analyze
    
    請特別留意 .isA<Type>() 上的泛型型別參數,並確保非同步斷言皆已正確加上 await(檢查是否有 unawaited_futures 警示)。
  • 執行測試:執行測試套件以驗證行為與斷言的執行期邏輯是否正確:
    dart test
    
    若測試失敗,請檢視 package:checks 提供的高詳細度失敗輸出資訊,以判斷是測試本身未通過,還是斷言條件轉換有誤。

關鍵語法差異與常見陷阱

[!IMPORTANT]
逐行直接轉換有時會引入隱蔽的 Bug 或導致虛假的測試通過。請務必仔細審視以下關鍵差異:

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

  • 舊版 Matcher:若引數為集合(List、Map、Set),expect(actual, expected)expect(actual, equals(expected)) 會執行深層相等性檢查(deep equality check)
  • Package Checks.equals(expected) 嚴格對應到 operator ==。由於 Dart 集合預設並未重寫 operator == 來比對內部元素,在集合上使用 .equals 將會檢查是否為同一物件實例(identity),在執行期幾乎必定失敗。
  • 修復方式:你必須將集合相等性斷言改為 .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. 同步與非同步 throws 的差異

  • 舊版 Matcher:在 package:matcher 中,無論是同步閉包還是非同步 Future,包裹在 expectexpectLater 內時,throwsA 的行為都很類似。
  • Package Checks:根據主體是同步還是非同步,.throws<E>() 斷言的行為與回傳型別會有所不同:
    • 同步Subject<T Function()>):.throws<E>() 會同步回傳一個 Subject<E>。此方法不接受回呼引數!你應直接在回傳的 Subject<E> 上進行鏈結或階層串接(cascade ..):
      // 正確寫法 (同步鏈結/串接)
      check(() => triggerSyncError()).throws<ArgumentError>()
        ..has((e) => e.message, 'message').equals('invalid input');
      
      // 錯誤寫法 (向同步 throws 傳遞回呼函式會導致編譯錯誤!)
      check(() => triggerSync").throws<ArgumentError>((it) => ...); // 編譯錯誤!
      
    • 非同步Subject<Future<T>>):.throws<E>() 會回傳 Future<void>。由於無法直接在 Future<void> 上繼續鏈結斷言,此情況必須傳入檢查回呼函式:
      // 正確寫法 (非同步回呼)
      await check(triggerAsyncError()).throws<ArgumentError>((it) => it
        ..has((e) => e.message, 'message').equals('invalid input'));
      
    • 關鍵陷阱:若嘗試在 await 的非同步 .throws<E>() 後方直接鏈結斷言(例如 await check(future).throws<E>().equals(...)),會因為其回傳型別為 Future<void> 而無法通過編譯。

6. RegExp / 樣式相等性

  • 舊版 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 鍵值包含判定(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)指定明確的泛型參數

  • 舊版 Matcherexpect(extensionTypeConst, 3) 因為鬆散的動態相等性判定而能正常編譯。
  • Package Checks:若 QrEciValue 是底層表示為 int 的擴充型別(例如 extension type const QrEciValue(int value) implements int),在 Subject<QrEciValue> 上呼叫 .equals(3) 會失敗,因為 3int)無法指派給 QrEciValue。若使用 as int 轉型,又會因為 QrEciValue 在靜態層面上已實作 int 而引發「Unnecessary cast」的靜態分析警告。
  • 修復方式:在 check 函式上明確指定泛型型別參數,強制 checks 將其視為原生基礎型別處理:
    // 正確寫法 (型別安全且不會產生警告)
    check<int>(QrEciValue.iso8859_1).equals(3);
    

10. 動態 Map / JSON 取值的型別轉換

  • 舊版 Matcher:鬆散的動態型別機制允許直接將靜態型別為 dynamic 的巢狀 JSON 尋找結果與 List 或 Map 進行比對。
  • Package Checks:嚴格的型別安全機制會在 .deepEquals(...) 中拒絕將 dynamic 隱式指派給 Iterable<Object?>
  • 修復方式:將動態取值結果顯式轉型(Cast)為 ListMap
    // 正確寫法 (顯式轉型為 List)
    check(myIterable).deepEquals(json['data']['items'] as List);
    

Matcher 對應 Checks 對照表

可將此表作為直接替換 Matcher 的快速對照參考:

舊版 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) 驗證物件記憶體識別性(Identity)
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