blob: 82c4e3c31426cb0dd1f36e667c686eca7a5d2bbc [file] [log] [blame] [view]
## Migrating from package:matcher
`package:checks` is currently in preview. Once this package reaches a stable
version, it will be the recommended package by the Dart team to use for most
tests.
[`package:matcher`][matcher] is the legacy package with an API exported from
`package:test/test.dart` and `package:test/expect.dart`.
**Do I have to migrate all at once?** No. `package:matcher` will be compatible
with `package:checks`, and old tests can continue to use matchers. Test cases
within the same file can use a mix of `expect` and `check`.
**_Should_ I migrate all at once?** Probably not, it depends on your tolerance
for having tests use a mix of APIs. As you add new tests, or need to make
updates to existing tests, using `checks` will make testing easier. Tests which
are stable and passing will not get significant benefits from a migration.
**Do I need to migrate at all?** No. When `package:test`stops exporting
these members it will be possible to add a dependency on `package:matcher` and
continue to use them. `package:matcher` will continue to be available.
**Why is the Dart team adding a second framework?** The `matcher` package has a
design which is fundamentally incompatible with using static types to validate
correct use. With an entirely new design, the static types in `checks` give
confidence that the expectation is appropriate for the value, and can narrow
autocomplete choices in the IDE for a better editing experience. The clean break
from the legacy implementation and API also gives an opportunity to make small
behavior and signature changes to align with modern Dart idioms.
**Should I start using checks right away?** There is still a
high potential for minor or major breaking changes during the preview window.
Once this package is stable, yes! The experience of using `checks` improves on
`matcher`. See some of the [improvements to look forward to in checks
below](#improvements-you-can-expect).
[matcher]: https://pub.dev/packages/matcher
## Trying Checks as a Preview
1. Add a `dev_dependency` on `checks: ^0.2.0`.
1. Replace the existing `package:test/test.dart` import with
`package:test/scaffolding.dart`.
1. Add an import to `package:checks/checks.dart`.
1. For an incremental migration within the test, add an import to
`package:test/expect.dart`. Remove it to surface errors in tests that still
need to be migrated, or keep it in so the tests work without being fully
migrated.
1. Migrate the test cases.
## Migrating from Matchers
Replace calls to `expect` or `expectLater` with a call to `check` passing the
first argument.
When a direct replacement is available, change the second argument from calling
a function returning a Matcher, to calling the relevant extension method on the
`Subject`.
Whenever you see a bare non-matcher value argument for `expected`, assume it
should use the `equals` expectation, although take care when the subject is a
collection.
See below, `.equals` may not always be the correct replacement in
`package:checks`.
```dart
expect(actual, expected);
check(actual).equals(expected);
// or maybe
check(actualCollection).deepEquals(expected);
await expectLater(actual, completes());
await check(actual).completes();
```
If you use the `reason` argument to `expect`, rename it to `because`.
```dart
expect(actual, expectation(), reason: 'some explanation');
check(because: 'some explanation', actual).expectation();
```
### Differences in behavior from matcher
- The `equals` Matcher performed a deep equality check on collections.
`.equals()` expectation will only correspond to [operator ==] so some tests
may need to replace `.equals()` with `.deepEquals()`.
- Streams must be explicitly wrapped into a `StreamQueue` before they can be
tested for behavior. Use `check(actualStream).withQueue`.
- `emitsAnyOf` is `Subject<StreamQueue>.anyOf`. `emitsInOrder` is `inOrder`.
The arguments are `FutureOr<void> Function(Subject<StreamQueue>)` and match
a behavior of the entire stream. In `matcher` the elements to expect could
have been a bare value to check for equality, a matcher for the emitted
value, or a matcher for the entire queue which would match multiple values.
Use `(s) => s.emits((e) => e.interestingCheck())` to check the emitted
elements.
- In `package:matcher` the [`matches` Matcher][matches] converted a `String`
argument into a `Regex`, so `matches(r'\d')` would match the value `'1'`.
This was potentially confusing, because even though `String` is a subtype of
`Pattern`, it wasn't used as a pattern directly.
With `matchesPattern` a `String` argument is used as a `Pattern` and
comparison uses [`String.allMatches`][allMatches].
For backwards compatibility change `matches(regexString)` to
`matchesPattern(RegExp(regexString))`.
- The `TypeMatcher.having` API is replace by the more general`.has`. While
`.having` could only be called on a `TypeMatcher` using `.isA`, `.has` works
on any `Subject`. `CoreChecks.has` takes 1 fewer arguments - instead of
taking the last argument, a `matcher` to apply to the field, it returns a
`Subject` for the field.
[matches]:https://pub.dev/documentation/matcher/latest/matcher/Matcher/matches.html
[allMatches]:https://api.dart.dev/stable/2.19.1/dart-core/Pattern/allMatches.html
### Matchers with replacements under a different name
- `anyElement` -> `Subject<Iterable>.any`
- `everyElement` -> `Subject<Iterable>.every`
- `completion(Matcher)` -> `completes(conditionCallback)`
- `containsPair(key, value)` -> Use `Subject<Map>[key].equals(value)`
- `hasLength(expected)` -> `length.equals(expected)`
- `isNot(Matcher)` -> `not(conditionCallback)`
- `pairwiseCompare` -> `pairwiseComparesTo`
- `same` -> `identicalTo`
- `stringContainsInOrder` -> `Subject<String>.containsInOrder`
### Members from `package:test/expect.dart` without a direct replacement
- `checks` does not ship with any type checking matchers for specific types.
Instead of, for example, `isArgumentError` use `isA<ArgumentError>`, and
similary `throws<ArgumentError>` over `throwsArgumentError`.
- `anything`. When a condition callback is needed that should accept any
value, pass `(_) {}`.
- Specific numeric comparison - `isNegative`, `isPositive`, `isZero` and their
inverses. Use `isLessThan`, `isGreaterThan`, `isLessOrEqual`, and
`isGreaterOrEqual` with appropriate numeric arguments.
- Numeric range comparison, `inClosedOpenRange`, `inExclusiveRange`,
`inInclusiveRange`, `inOpenClosedRange`. Use cascades to chain a check for
both ends of the range onto the same subject.
- `containsOnce`: TODO add missing expectation
- `emitsInAnyOrder`: TODO add missing expectation
- `expectAsync` and `expectAsyncUntil`. Continue to import
`package:test/expect.dart` for these APIs.
- `isIn`: TODO add missing expectation
- `orderedEquals`: Use `deepEquals`. If the equality needs to specifically
*not* be deep equality (this is unusual, nested collections are unlikely to
have a meaningful equality), force using `operator ==` at the first level
with `.deepEquals(expected.map((e) => (Subject<Object?> s) => s.equals(e)))`;
- `prints`: TODO add missing expectation? Is this one worth replacing?
- `predicate`: TODO add missing expectation
## Improvements you can expect
Expectations are statically restricted to those which are appropriate for the
type. So while the following is statically allowed with `matcher` but always
fails at runtime, the expectation cannot be written at all with `checks`.
```dart
expect(1, contains(1)); // No static error, always fails
check(1).contains(1); // Static error. The method 'contains' isn't defined
```
These static restrictions also improve the relevance of IDE autocomplete
suggestions. While editing with the cursor at `_`, the suggestions provided
in the `matcher` example can include _any_ top level element including matchers
appropriate for other types of value, type names, and top level definitions from
other packages. With the cursor following a `.` in the `checks` example the
suggestions will only be expectations or utilities appropriate for the value
type.
```dart
expect(actual, _ // many unrelated suggestions
check(actual)._ // specific suggestions
```
Asynchronous matchers in `matcher` are a subtype of synchronous matchers, but do
not satisfy the same behavior contract. Some APIs which use a matcher could not
validate whether it would satisfy the behavior it needs, and it could result in
a false success, false failure, or misleading errors. APIs which correctly use
asynchronous matchers need to do a type check and change their interaction based
on the runtime type. Asynchronous expectations in `checks` are refused at
runtime when a synchronous answer is required. The error will help solve the
specific misuse, instead of resulting in a confusing error, or worse a missed
failure. The reason for the poor compatibility in `matcher` is due to some
history of implementation - asynchronous matchers were written in `test`
alongside `expect`, and synchronous matchers have no dependency on the
asynchronous implementation.
Asynchronous expectations always return a `Future`, and with the
[`unawaited_futures` lint][unawaited lint] should more safely ensure that
asynchronous expectation work is completed within the test body. With `matcher`
it was up to the author to correctly use `await expecLater` for asynchronous
cases, and `expect` for synchronous cases, and if `expect` was used with an
asynchronous matcher the expectation could fail at any point.
[unawaited lint]: https://dart.dev/lints/unawaited_futures