$npx -y skills add kevmoo/dash_skills --skill dart-matcher-best-practicesBest practices for using expect and package:matcher. Focuses on readable assertions, proper matcher selection, and avoiding common pitfalls.
| 1 | # Dart Matcher Best Practices |
| 2 | |
| 3 | ## When to use this skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Writing assertions using `expect` and `package:matcher`. |
| 7 | - Migrating legacy manual checks to cleaner matchers. |
| 8 | - Debugging confusing test failures. |
| 9 | |
| 10 | ## Discovery |
| 11 | |
| 12 | To find candidates for improving matcher usage, search for suboptimal patterns: |
| 13 | |
| 14 | ### Suboptimal Length Checks |
| 15 | Search for length checks that should use `hasLength`: |
| 16 | - **Regex**: `expect\([^,]+.length,\s*` |
| 17 | |
| 18 | ### Suboptimal Boolean Checks |
| 19 | Search for checks on boolean properties that have specific matchers: |
| 20 | - **Regex**: `expect\([^,]+.isEmpty,\s*(true|equals\(true\))` |
| 21 | - **Regex**: `expect\([^,]+.isNotEmpty,\s*(true|equals\(true\))` |
| 22 | - **Regex**: `expect\([^,]+.contains\(.*\),\s*(true|equals\(true\))` |
| 23 | |
| 24 | ### Suboptimal Map Lookups |
| 25 | Search for manual map lookups instead of `containsPair`: |
| 26 | - **Regex**: `expect\([^,]+\[.*\],\s*` |
| 27 | |
| 28 | ## Core Matchers |
| 29 | |
| 30 | ### 1. Collections (`hasLength`, `contains`, `isEmpty`, `unorderedEquals`, `containsPair`) |
| 31 | |
| 32 | - **`hasLength(n)`**: |
| 33 | - Prefer `expect(list, hasLength(n))` over `expect(list.length, n)`. |
| 34 | - Gives better error messages on failure (shows actual list content). |
| 35 | |
| 36 | - **`isEmpty` / `isNotEmpty`**: |
| 37 | - Prefer `expect(list, isEmpty)` over `expect(list.isEmpty, true)`. |
| 38 | - Prefer `expect(list, isNotEmpty)` over `expect(list.isNotEmpty, true)`. |
| 39 | |
| 40 | - **`contains(item)`**: |
| 41 | - Verify existence without manual iteration. |
| 42 | - Prefer over `expect(list.contains(item), true)`. |
| 43 | |
| 44 | - **`unorderedEquals(items)`**: |
| 45 | - Verify contents regardless of order. |
| 46 | - Prefer over `expect(list, containsAll(items))`. |
| 47 | |
| 48 | - **`containsPair(key, value)`**: |
| 49 | - Verify a map contains a specific key-value pair. |
| 50 | - Prefer over checking `expect(map[key], value)` or |
| 51 | `expect(map.containsKey(key), true)`. |
| 52 | |
| 53 | ### 2. Type Checks (`isA<T>` and `TypeMatcher<T>`) |
| 54 | |
| 55 | - **`isA<T>()`**: |
| 56 | - Prefer for inline assertions: `expect(obj, isA<Type>())`. |
| 57 | - More concise and readable than `TypeMatcher<Type>()`. |
| 58 | - Allows chaining constraints using `.having()`. |
| 59 | |
| 60 | - **`TypeMatcher<T>`**: |
| 61 | - Prefer when defining top-level reusable matchers. |
| 62 | - **Use `const`**: `const isMyType = TypeMatcher<MyType>();` |
| 63 | - Chaining `.having()` works here too, but the resulting matcher is not `const`. |
| 64 | |
| 65 | ### 3. Object Properties (`having`) |
| 66 | |
| 67 | Use `.having()` on `isA<T>()` or other TypeMatchers to check properties. |
| 68 | |
| 69 | - **Descriptive Names**: Use meaningful parameter names in the closure (e.g., |
| 70 | `(e) => e.message`) instead of generic ones like `p0` to improve readability. |
| 71 | |
| 72 | ```dart |
| 73 | expect(person, isA<Person>() |
| 74 | .having((p) => p.name, 'name', 'Alice') |
| 75 | .having((p) => p.age, 'age', greaterThan(18))); |
| 76 | ``` |
| 77 | |
| 78 | This provides detailed failure messages indicating exactly which property |
| 79 | failed. |
| 80 | |
| 81 | ### 4. Async Assertions |
| 82 | |
| 83 | - **`completion(matcher)`**: |
| 84 | - Wait for a future to complete and check its value. |
| 85 | - **Prefer `await expectLater(...)`** to ensure the future completes before |
| 86 | the test continues. |
| 87 | - `await expectLater(future, completion(equals(42)))`. |
| 88 | |
| 89 | - **`throwsA(matcher)`**: |
| 90 | - Check that a future or function throws an exception. |
| 91 | - `await expectLater(future, throwsA(isA<StateError>()))`. |
| 92 | - `expect(() => function(), throwsA(isA<ArgumentError>()))` (synchronous |
| 93 | function throwing is fine with `expect`). |
| 94 | |
| 95 | ### 5. Using `expectLater` |
| 96 | |
| 97 | Use `await expectLater(...)` when testing async behavior to ensure proper |
| 98 | sequencing. |
| 99 | |
| 100 | ```dart |
| 101 | // GOOD: Waits for future to complete before checking side effects |
| 102 | await expectLater(future, completion(equals(42))); |
| 103 | expect(sideEffectState, equals('done')); |
| 104 | |
| 105 | // BAD: Side effect check might run before future completes |
| 106 | expect(future, completion(equals(42))); |
| 107 | expect(sideEffectState, equals('done')); // Race condition! |
| 108 | ``` |
| 109 | |
| 110 | ## Principles |
| 111 | |
| 112 | 1. **Readable Failures**: Choose matchers that produce clear error messages. |
| 113 | 2. **Avoid Manual Logic**: Don't use `if` statements or `for` loops for |
| 114 | assertions; let matchers handle it. |
| 115 | 3. **Specific Matchers**: Use the most specific matcher available (e.g., |
| 116 | `containsPair` for maps instead of checking keys manually). |
| 117 | |
| 118 | ## Related Skills |
| 119 | |
| 120 | - **[dart-test-fundamentals]**: Core |
| 121 | concepts for structuring tests, lifecycles, and configuration. |
| 122 | |
| 123 | [dart-test-fundamentals]: https://github.com/kevmoo/dash_skills/blob/main/skills/dart-test-fundamentals/SKILL.md |