Claude Skill

dart-matcher-best-practices

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

LLM Mart · 0 points · 1 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download kevmoo-dash_skills-skills_dart-matcher-best-practices-38dce74.zip · 3 KB
Part of kevmoo/dash_skills — 12 skills

Install

skills CLI npx skills add https://github.com/kevmoo/dash_skills/tree/main/skills/dart-matcher-best-practices
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install kevmoo-dash-skills@llmmart
Git 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 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

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.

No comments yet.

Reviews (0)

No reviews yet.

Related