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
onchecks: ^0.2.0
. -
Replace the existing
package:test/test.dart
import withpackage: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();
- 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. Usecheck(actualStream).withQueue
. emitsAnyOf
isSubject<StreamQueue>.anyOf
.emitsInOrder
isinOrder
. The arguments areFutureOr<void> Function(Subject<StreamQueue>)
and match a behavior of the entire stream. Inmatcher
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
thematches
Matcher converted aString
argument into aRegex
, somatches(r'\d')
would match the value'1'
. This was potentially confusing, because even thoughString
is a subtype ofPattern
, it wasn't used as a pattern directly. WithmatchesPattern
aString
argument is used as aPattern
and comparison usesString.allMatches
. For backwards compatibility changematches(regexString)
tomatchesPattern(RegExp(regexString))
. - The
TypeMatcher.having
API is replace by the more general.has
. While.having
could only be called on aTypeMatcher
using.isA
,.has
works on anySubject
.CoreChecks.has
takes 1 fewer arguments - instead of taking the last argument, amatcher
to apply to the field, it returns aSubject
for the field.
anyElement
->Subject<Iterable>.any
everyElement
->Subject<Iterable>.every
completion(Matcher)
->completes(conditionCallback)
containsPair(key, value)
-> UseSubject<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 (==
).
checks
does not ship with any type checking matchers for specific types. Instead of, for example,isArgumentError
useisA<ArgumentError>
, and similarythrows<ArgumentError>
overthrowsArgumentError
.anything
. When a condition callback is needed that should accept any value, pass(_) {}
.- Specific numeric comparison -
isNegative
,isPositive
,isZero
and their inverses. UseisLessThan
,isGreaterThan
,isLessOrEqual
, andisGreaterOrEqual
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 expectationemitsInAnyOrder
: TODO add missing expectationexpectAsync
andexpectAsyncUntil
. Continue to importpackage:test/expect.dart
for these APIs.isIn
: TODO add missing expectationorderedEquals
: UsedeepEquals
. If the equality needs to specifically not be deep equality (this is unusual, nested collections are unlikely to have a meaningful equality), force usingoperator ==
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
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
.
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.