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:checks為dev_dependency: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. 驗證與回饋機制
- 靜態分析:對目標套件執行靜態分析:
請特別留意dart analyze.isA<Type>()上的泛型型別參數,並確保非同步斷言皆已正確加上await(檢查是否有unawaited_futures警示)。 - 執行測試:執行測試套件以驗證行為與斷言的執行期邏輯是否正確:
若測試失敗,請檢視dart testpackage: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)
- 舊版 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. 同步與非同步 throws 的差異
- 舊版 Matcher:在
package:matcher中,無論是同步閉包還是非同步 Future,包裹在expect或expectLater內時,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:在靜態層面上,
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 鍵值包含判定(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 const QrEciValue(int value) implements int),在Subject<QrEciValue>上呼叫.equals(3)會失敗,因為3(int)無法指派給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)為
List或Map:// 正確寫法 (顯式轉型為 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) |






