dart-matcher-best-practices

v2026.09.24

Best practices for using `expect` and `package:matcher`. Focuses on readable assertions, proper matcher selection, and avoiding common pitfalls.

GitHub
安装命令
npx skhub add kevmoo/dart-matcher-best-practices
Markdown
SKILL.md

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.
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:
      // 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 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

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

Apache-2.0

源路径

skills/dart-matcher-best-practices

默认分支

main

最新提交

0b6371c

Tree SHA

76e74e4