mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 13:43:38 +02:00
Installs the official flutter/skills and dart-lang/skills packs into .agents/skills/ and appends Flutter's AI rules.md to CLAUDE.md, so Claude Code has task-specific playbooks alongside the dart MCP server (registered separately at user scope). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NqCvrbFnoNNmqCwZKntBK
529 lines
19 KiB
Markdown
529 lines
19 KiB
Markdown
---
|
|
name: dart-migrate-to-checks-package
|
|
description: |-
|
|
Replace the usage of `expect` and similar functions from `package:matcher`
|
|
to `package:checks` equivalents.
|
|
metadata:
|
|
model: models/gemini-3.1-pro-preview
|
|
last_modified: Tue, 09 Jun 2026 19:30:00 GMT
|
|
---
|
|
# Migrating Dart Tests to 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.
|
|
|
|
## Contents
|
|
- [When to Use This Skill](#when-to-use-this-skill)
|
|
- [How to Use This Skill (The Workflow)](#how-to-use-this-skill-the-workflow)
|
|
- [Key Syntax Differences and Pitfalls](#key-syntax-differences-and-pitfalls)
|
|
- [Matcher-to-Checks Mapping Table](#matcher-to-checks-mapping-table)
|
|
- [Matchers with No Direct Replacements](#matchers-with-no-direct-replacements)
|
|
- [Strategies for Discovery](#strategies-for-discovery)
|
|
- [Examples](#examples)
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
---
|
|
|
|
## How to Use This Skill (The Workflow)
|
|
|
|
Follow this structured workflow to safely and systematically migrate a test suite:
|
|
|
|
### 1. Dependency Setup
|
|
- 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).
|
|
|
|
### 2. Identify and Plan Target Files
|
|
- Use the grep patterns in [Strategies for Discovery](#strategies-for-discovery)
|
|
to locate all test files containing legacy `expect` or `expectLater` calls.
|
|
- Decide whether to migrate files fully or incrementally.
|
|
|
|
### 3. Migrating a File (Incremental or Full)
|
|
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](#key-syntax-differences-and-pitfalls) and the
|
|
[Matcher-to-Checks Mapping Table](#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.
|
|
|
|
### 4. Verification and Feedback Loops
|
|
- **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.
|
|
|
|
---
|
|
|
|
## 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:
|
|
|
|
### 1. Collection Equality Pitfall (`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]);
|
|
```
|
|
|
|
### 2. The `reason` Parameter is now `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();
|
|
```
|
|
|
|
### 3. Regular Expression Matching (`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+'));
|
|
```
|
|
|
|
### 4. Property Extraction (`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');
|
|
```
|
|
|
|
### 5. Synchronous vs. Asynchronous `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>`.
|
|
|
|
### 6. RegExp / Pattern Equality
|
|
- **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();
|
|
```
|
|
|
|
### 7. Strict Nullable Boolean Safety (`bool?` fields)
|
|
- **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);
|
|
```
|
|
|
|
### 8. Map Key Containment (`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');
|
|
```
|
|
|
|
### 9. Explicit Generic Parameters for Extension Types
|
|
- **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);
|
|
```
|
|
|
|
### 10. Dynamic Map / JSON Lookup Casting
|
|
- **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-to-Checks Mapping Table
|
|
|
|
Use this table as a quick reference for direct matcher replacements:
|
|
|
|
| Legacy Matcher | Package Checks Equivalent | Notes |
|
|
| :--- | :--- | :--- |
|
|
| `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(...)` | |
|
|
|
|
---
|
|
|
|
## Matchers with No Direct Replacements
|
|
|
|
Some legacy matchers do not have a one-to-one equivalent in `package:checks`
|
|
due to API cleanup. Use these standard workarounds:
|
|
|
|
### 1. Specific Error Matchers
|
|
- **Legacy**: `throwsArgumentError`, `throwsStateError`,
|
|
`throwsUnsupportedError`, etc.
|
|
- **Checks**: Use `.throws<T>()` with the specific error type:
|
|
```dart
|
|
await check(triggerError()).throws<ArgumentError>();
|
|
```
|
|
|
|
### 2. The `anything` Matcher
|
|
- **Legacy**: `expect(actual, anything)`
|
|
- **Checks**: Pass an empty condition callback `(_) {}` when a condition is
|
|
syntactically required:
|
|
```dart
|
|
await check(someFuture).completes((_) {});
|
|
```
|
|
|
|
### 3. Specific Numeric Toggles
|
|
- **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)`
|
|
|
|
### 4. Numeric Ranges
|
|
- **Legacy**: `inClosedOpenRange(min, max)`, `inInclusiveRange(min, max)`,
|
|
etc.
|
|
- **Checks**: Chain the boundaries using the cascade operator (`..`):
|
|
```dart
|
|
check(actualValue)
|
|
..isGreaterOrEqual(min)
|
|
..isLessThan(max);
|
|
```
|
|
|
|
---
|
|
|
|
## Writing Custom Expectations (Replacing Custom Matchers)
|
|
|
|
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';
|
|
```
|
|
|
|
### 1. Simple Custom Expectations (using `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'],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Nested Property Extraction (using `nest` or `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);
|
|
},
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3. Asynchronous Custom Expectations
|
|
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']);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Strategies for Discovery
|
|
|
|
Execute these commands in the terminal to identify legacy matchers and files
|
|
requiring migration:
|
|
|
|
```bash
|
|
# 1. Find all test files containing legacy expect() or expectLater()
|
|
grep -rn "expect(" test/
|
|
grep -rn "expectLater(" test/
|
|
|
|
# 2. Find potential collection equality pitfalls (literal lists or maps)
|
|
grep -rn "expect(.*, \[" test/
|
|
grep -rn "expect(.*, {" test/
|
|
|
|
# 3. Find matches() calls (need conversion to RegExp + matchesPattern)
|
|
grep -rn "matches(" test/
|
|
|
|
# 4. Find legacy TypeMatcher.having() calls (which need conversion to .has())
|
|
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');
|
|
```
|
|
|
|
### 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});
|
|
```
|
|
|
|
### 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');
|
|
```
|
|
|
|
### Complex Property Matching (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');
|
|
```
|
|
|
|
### Asynchronous Futures
|
|
**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>();
|
|
```
|
|
|
|
### Asynchronous Streams
|
|
**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)),
|
|
]);
|
|
```
|