dart-matcher-best-practices
Best practices for using `expect` and `package:matcher`. Focuses on readable assertions, proper matcher selection, and avoiding common pitfalls.
Install
npx skills add https://github.com/kevmoo/dash_skills/tree/main/skills/dart-matcher-best-practices
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kevmoo-dash-skills@llmmart
git clone https://github.com/kevmoo/dash_skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole kevmoo/dash_skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Dart Matcher Best Practices
When to use this skill
Use this skill when:
- Writing assertions using
expectandpackage:matcher. - Migrating legacy manual checks to cleaner matchers.
- Debugging confusing test failures.
When NOT to use (Abstention Guardrails)
Do NOT apply this skill or refactor assertions when:
- Non-Collection Properties: The property happens to be named
.lengthbut belongs to a non-collection class (e.g. geometric scalar magnitude on aVector, duration on a buffer, or line segment distance).hasLength(n)expects aninton anIterable,Map, orString. - Custom Domain Booleans: The boolean check tests an arbitrary domain
property (e.g.
expect(switch.isEnabled, isTrue)). Do not invent non-existent matchers. - Alternative Assertion Frameworks: The package has adopted
package:checks(check(x)...). Do not revertchecksback topackage:matcher. - Already Idiomatic Assertions: Tests already use declarative matchers
(
hasLength,isEmpty,isNotEmpty,contains,containsPair,isA,throwsA). Abstain and make zero modifications.
Discovery
To find candidates for improving matcher usage, search for suboptimal patterns:
Suboptimal Length Checks
Search for length checks that should use hasLength:
- Regex:
expect\([^,]+.length,\s*
Suboptimal Boolean Checks
Search for checks on boolean properties that have specific matchers:
- Regex:
expect\([^,]+.isEmpty,\s*(true|equals\(true\)) - Regex:
expect\([^,]+.isNotEmpty,\s*(true|equals\(true\)) - Regex:
expect\([^,]+.contains\(.*\),\s*(true|equals\(true\))
Suboptimal Map Lookups
Search for manual map lookups instead of containsPair:
- Regex:
expect\([^,]+\[.*\],\s*
Core Matchers
1. Collections (hasLength, contains, isEmpty, unorderedEquals, containsPair)
hasLength(n):- Prefer
expect(list, hasLength(n))overexpect(list.length, n). - Gives better error messages on failure (shows actual list content).
- Prefer
isEmpty/isNotEmpty:- Prefer
expect(list, isEmpty)overexpect(list.isEmpty, true). - Prefer
expect(list, isNotEmpty)overexpect(list.isNotEmpty, true).
- Prefer
contains(item):- Verify existence without manual iteration.
- Prefer over
expect(list.contains(item), true).
unorderedEquals(items):- Verify contents regardless of order.
- Prefer over
expect(list, containsAll(items)).
containsPair(key, value):- Verify a map contains a specific key-value pair.
- Prefer over checking
expect(map[key], value)orexpect(map.containsKey(key), true).
2. Type Checks (isA<T> and TypeMatcher<T>)
isA<T>():- Prefer for inline assertions:
expect(obj, isA<Type>()). - More concise and readable than
TypeMatcher<Type>(). - Allows chaining constraints using
.having().
- Prefer for inline assertions:
TypeMatcher<T>:- Prefer when defining top-level reusable matchers.
- Use
const:const isMyType = TypeMatcher<MyType>(); - Chaining
.having()works here too, but the resulting matcher is notconst.
3. Object Properties (having)
Use .having() on isA<T>() or other TypeMatchers to check properties.
- Descriptive Names: Use meaningful parameter names in the closure (e.g.,
(e) => e.message) instead of generic ones likep0to improve readability.
expect(person, isA<Person>()
.having((p) => p.name, 'name', 'Alice')
.having((p) => p.age, 'age', greaterThan(18)));
This provides detailed failure messages indicating exactly which property failed.
4. Async Assertions
completion(matcher):- Wait for a future to complete and check its value.
- Prefer
await expectLater(...)to ensure the future completes before the test continues. await expectLater(future, completion(equals(42))).
throwsA(matcher):- Check that a future or function throws an exception.
await expectLater(future, throwsA(isA<StateError>())).expect(() => function(), throwsA(isA<ArgumentError>()))(synchronous function throwing is fine withexpect).
5. Exception Testing (Avoid try/catch with fail())
- Prefer
throwsAover imperativetry/catch:- Legacy tests often use
try { fn(); fail('should throw'); } catch (e) { expect(e, isA<T>()); }. - Replace with declarative matchers:
// Synchronous: expect(() => parseData('invalid'), throwsA(isA<FormatException>())); // Asynchronous: await expectLater(client.fetch('bad-url'), throwsA(isA<HttpException>())); - Common built-in exception matchers include
throwsArgumentError,throwsStateError,throwsRangeError,throwsFormatException,throwsNoSuchMethodError, andthrowsUnsupportedError.
- Legacy tests often use
6. Using expectLater
Use await expectLater(...) when testing async behavior to ensure proper
sequencing.
// GOOD: Waits for future to complete before checking side effects
await expectLater(future, completion(equals(42)));
expect(sideEffectState, equals('done'));
// BAD: Side effect check might run before future completes
expect(future, completion(equals(42)));
expect(sideEffectState, equals('done')); // Race condition!
Principles
- Readable Failures: Choose matchers that produce clear error messages.
- Avoid Manual Logic: Don't use
ifstatements orforloops for assertions; let matchers handle it. - Specific Matchers: Use the most specific matcher available (e.g.,
containsPairfor maps instead of checking keys manually).
Related Skills
- dart-test-fundamentals: Core concepts for structuring tests, lifecycles, and configuration.
Files (dash_skills)
-
evals
-
evals.json 3.7 KB
{ "repo_criteria": [ "evals/code_quality_rubric.json" ], "evals": [ { "id": 1, "prompt": "Refactor the unit tests in test/collection_test.dart to replace legacy boolean checks and property lookups (like .length == n, .isEmpty == true, .contains(x) == true, and map[key] == val) with idiomatic package:matcher matchers (hasLength, isEmpty, contains, and containsPair).", "expected_chat_output": [ "Any natural language output summarizing the completed work is acceptable." ], "expected_repo_state": [ "Checks on '.length' are replaced with 'hasLength(n)'.", "Checks on '.isEmpty' or '.isNotEmpty' are replaced with 'isEmpty' or 'isNotEmpty'.", "Checks on '.contains(item)' are replaced with 'contains(item)'.", "Map key/value lookups are replaced with 'containsPair(key, value)'.", "The refactored test file compiles with zero errors and passes static analysis ('dart analyze --fatal-infos').", "All test assertions pass successfully when running 'dart test'." ], "agent_config": "bare-agent" }, { "id": 2, "prompt": "Refactor the custom exception tests in test/api_client_test.dart to replace type checks and subsequent property assertions on the casted exception with chained isA<T>().having() matchers.", "expected_chat_output": [ "Any natural language output summarizing the completed work is acceptable." ], "expected_repo_state": [ "Type checks ('is CustomException') combined with manual field assertions are replaced with 'isA<CustomException>()'.", "Property inspections are chained using '.having((e) => e.property, 'property', expectedValue)'.", "Descriptive parameter names are used in closures instead of generic positional names.", "The refactored test file compiles with zero errors and passes static analysis ('dart analyze --fatal-infos').", "All test assertions pass successfully when running 'dart test'." ], "agent_config": "bare-agent" }, { "id": 3, "prompt": "Modernize the async error-handling tests in test/async_loader_test.dart by replacing imperative try/catch blocks with 'await expectLater(future, throwsA(isA<T>()))' and ensuring future completions are awaited.", "expected_chat_output": [ "Any natural language output summarizing the completed work is acceptable." ], "expected_repo_state": [ "Imperative try/catch blocks that invoke fail() are replaced with 'await expectLater(..., throwsA(isA<T>()))' or 'expect(() => syncCall(), throwsA(isA<T>()))'.", "Asynchronous completion checks use 'await expectLater(..., completion(...))' to prevent race conditions.", "The refactored test file compiles with zero errors and passes static analysis ('dart analyze --fatal-infos').", "All test assertions pass successfully when running 'dart test'." ], "agent_config": "bare-agent" }, { "id": 4, "prompt": "Review test/non_collection_test.dart and determine if package:matcher best practices should be applied to refactor any assertions.", "expected_chat_output": [ "The agent must explicitly state that no changes are needed or abstain from modifying non-collection length checks and already idiomatic assertions." ], "expected_repo_state": [ "No modifications are made to test/non_collection_test.dart.", "Scalar properties and non-collection length assertions remain unchanged.", "The test file compiles with zero errors and passes static analysis ('dart analyze --fatal-infos').", "All test assertions pass successfully when running 'dart test'." ], "agent_config": "bare-agent" } ] }
-
-
SKILL.md 6.1 KB
--- name: dart-matcher-best-practices description: |- Best practices for using `expect` and `package:matcher`. Focuses on readable assertions, proper matcher selection, and avoiding common pitfalls. license: Apache-2.0 key_features: - Package matcher assertions - Readable test expectations - Matcher selection & refactoring --- # Dart Matcher Best Practices ## When to use this skill Use this skill when: - Writing assertions using `expect` and `package:matcher`. - Migrating legacy manual checks to cleaner matchers. - Debugging confusing test failures. ### When NOT to use (Abstention Guardrails) Do NOT apply this skill or refactor assertions when: - **Non-Collection Properties**: The property happens to be named `.length` but belongs to a non-collection class (e.g. geometric scalar magnitude on a `Vector`, duration on a buffer, or line segment distance). `hasLength(n)` expects an `int` on an `Iterable`, `Map`, or `String`. - **Custom Domain Booleans**: The boolean check tests an arbitrary domain property (e.g. `expect(switch.isEnabled, isTrue)`). Do not invent non-existent matchers. - **Alternative Assertion Frameworks**: The package has adopted `package:checks` (`check(x)...`). Do not revert `checks` back to `package:matcher`. - **Already Idiomatic Assertions**: Tests already use declarative matchers (`hasLength`, `isEmpty`, `isNotEmpty`, `contains`, `containsPair`, `isA`, `throwsA`). Abstain and make zero modifications. ## Discovery To find candidates for improving matcher usage, search for suboptimal patterns: ### Suboptimal Length Checks Search for length checks that should use `hasLength`: - **Regex**: `expect\([^,]+.length,\s*` ### Suboptimal Boolean Checks Search for checks on boolean properties that have specific matchers: - **Regex**: `expect\([^,]+.isEmpty,\s*(true|equals\(true\))` - **Regex**: `expect\([^,]+.isNotEmpty,\s*(true|equals\(true\))` - **Regex**: `expect\([^,]+.contains\(.*\),\s*(true|equals\(true\))` ### Suboptimal Map Lookups Search for manual map lookups instead of `containsPair`: - **Regex**: `expect\([^,]+\[.*\],\s*` ## Core Matchers ### 1. Collections (`hasLength`, `contains`, `isEmpty`, `unorderedEquals`, `containsPair`) - **`hasLength(n)`**: - Prefer `expect(list, hasLength(n))` over `expect(list.length, n)`. - Gives better error messages on failure (shows actual list content). - **`isEmpty` / `isNotEmpty`**: - Prefer `expect(list, isEmpty)` over `expect(list.isEmpty, true)`. - Prefer `expect(list, isNotEmpty)` over `expect(list.isNotEmpty, true)`. - **`contains(item)`**: - Verify existence without manual iteration. - Prefer over `expect(list.contains(item), true)`. - **`unorderedEquals(items)`**: - Verify contents regardless of order. - Prefer over `expect(list, containsAll(items))`. - **`containsPair(key, value)`**: - Verify a map contains a specific key-value pair. - Prefer over checking `expect(map[key], value)` or `expect(map.containsKey(key), true)`. ### 2. Type Checks (`isA<T>` and `TypeMatcher<T>`) - **`isA<T>()`**: - Prefer for inline assertions: `expect(obj, isA<Type>())`. - More concise and readable than `TypeMatcher<Type>()`. - Allows chaining constraints using `.having()`. - **`TypeMatcher<T>`**: - Prefer when defining top-level reusable matchers. - **Use `const`**: `const isMyType = TypeMatcher<MyType>();` - Chaining `.having()` works here too, but the resulting matcher is not `const`. ### 3. Object Properties (`having`) Use `.having()` on `isA<T>()` or other TypeMatchers to check properties. - **Descriptive Names**: Use meaningful parameter names in the closure (e.g., `(e) => e.message`) instead of generic ones like `p0` to improve readability. ```dart expect(person, isA<Person>() .having((p) => p.name, 'name', 'Alice') .having((p) => p.age, 'age', greaterThan(18))); ``` This provides detailed failure messages indicating exactly which property failed. ### 4. Async Assertions - **`completion(matcher)`**: - Wait for a future to complete and check its value. - **Prefer `await expectLater(...)`** to ensure the future completes before the test continues. - `await expectLater(future, completion(equals(42)))`. - **`throwsA(matcher)`**: - Check that a future or function throws an exception. - `await expectLater(future, throwsA(isA<StateError>()))`. - `expect(() => function(), throwsA(isA<ArgumentError>()))` (synchronous function throwing is fine with `expect`). ### 5. Exception Testing (Avoid `try/catch` with `fail()`) - **Prefer `throwsA` over imperative `try/catch`**: - Legacy tests often use `try { fn(); fail('should throw'); } catch (e) { expect(e, isA<T>()); }`. - Replace with declarative matchers: ```dart // Synchronous: expect(() => parseData('invalid'), throwsA(isA<FormatException>())); // Asynchronous: await expectLater(client.fetch('bad-url'), throwsA(isA<HttpException>())); ``` - Common built-in exception matchers include `throwsArgumentError`, `throwsStateError`, `throwsRangeError`, `throwsFormatException`, `throwsNoSuchMethodError`, and `throwsUnsupportedError`. ### 6. Using `expectLater` Use `await expectLater(...)` when testing async behavior to ensure proper sequencing. ```dart // GOOD: Waits for future to complete before checking side effects await expectLater(future, completion(equals(42))); expect(sideEffectState, equals('done')); // BAD: Side effect check might run before future completes expect(future, completion(equals(42))); expect(sideEffectState, equals('done')); // Race condition! ``` ## Principles 1. **Readable Failures**: Choose matchers that produce clear error messages. 2. **Avoid Manual Logic**: Don't use `if` statements or `for` loops for assertions; let matchers handle it. 3. **Specific Matchers**: Use the most specific matcher available (e.g., `containsPair` for maps instead of checking keys manually). ## Related Skills - **[dart-test-fundamentals]**: Core concepts for structuring tests, lifecycles, and configuration. [dart-test-fundamentals]: https://github.com/kevmoo/dash_skills/blob/main/skills/dart-test-fundamentals/SKILL.md
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.