Skill v1.0.1
currentAutomated scan100/100+1 new
version: "1.0.1" 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
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 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))overexpect(list.length, n). - Gives better error messages on failure (shows actual list content).
- `isEmpty` / `isNotEmpty`:
- Prefer
expect(list, isEmpty)overexpect(list.isEmpty, true). - Prefer
expect(list, isNotEmpty)overexpect(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.
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.
// GOOD: Waits for future to complete before checking side effectsawait expectLater(future, completion(equals(42)));expect(sideEffectState, equals('done'));// BAD: Side effect check might run before future completesexpect(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.,
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