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
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.
Add a dev_dependency
on checks: ^0.2.0
.
Replace the existing package:test/test.dart
import with package:test/scaffolding.dart
.
Add an import to package:checks/checks.dart
.
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.
Migrate the test cases.
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
.
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
.
expect(actual, expectation(), reason: 'some explanation'); check(because: 'some explanation', actual).expectation();
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()
.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.package:matcher
the matches
Matcher 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
. For backwards compatibility change matches(regexString)
to matchesPattern(RegExp(regexString))
.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.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
-> pairwiseMatches
same
-> identicalTo
stringContainsInOrder
-> Subject<String>.containsInOrder
containsAllInOrder(iterable)
-> Subject<Iterable>.containsMatchingInOrder(iterable)
to compare with conditions other than equals, Subject<Iterable>.containsEqualInOrder(iterable)
to compare each index with the equality operator (==
).package:test/expect.dart
without a direct replacementchecks
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 (_) {}
.isNegative
, isPositive
, isZero
and their inverses. Use isLessThan
, isGreaterThan
, isLessOrEqual
, and isGreaterOrEqual
with appropriate numeric arguments.inClosedOpenRange
, inExclusiveRange
, inInclusiveRange
, inOpenClosedRange
. Use cascades to chain a check for both ends of the range onto the same subject.containsOnce
: TODO add missing expectationemitsInAnyOrder
: TODO add missing expectationexpectAsync
and expectAsyncUntil
. Continue to import package:test/expect.dart
for these APIs.isIn
: TODO add missing expectationorderedEquals
: 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 expectationExpectations 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
.
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.
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 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.