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:teststops 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_dependencyonchecks: ^0.2.0. -
Replace the existing
package:test/test.dartimport 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
equalsMatcher 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
StreamQueuebefore they can be tested for behavior. Usecheck(actualStream).withQueue. emitsAnyOfisSubject<StreamQueue>.anyOf.emitsInOrderisinOrder. The arguments areFutureOr<void> Function(Subject<StreamQueue>)and match a behavior of the entire stream. Inmatcherthe 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:matcherthematchesMatcher converted aStringargument into aRegex, somatches(r'\d')would match the value'1'. This was potentially confusing, because even thoughStringis a subtype ofPattern, it wasn't used as a pattern directly. WithmatchesPatternaStringargument is used as aPatternand comparison usesString.allMatches. For backwards compatibility changematches(regexString)tomatchesPattern(RegExp(regexString)). - The
TypeMatcher.havingAPI is replace by the more general.has. While.havingcould only be called on aTypeMatcherusing.isA,.hasworks on anySubject.CoreChecks.hastakes 1 fewer arguments - instead of taking the last argument, amatcherto apply to the field, it returns aSubjectfor the field.
anyElement->Subject<Iterable>.anyeveryElement->Subject<Iterable>.everycompletion(Matcher)->completes(conditionCallback)containsPair(key, value)-> UseSubject<Map>[key].equals(value)hasLength(expected)->length.equals(expected)isNot(Matcher)->not(conditionCallback)pairwiseCompare->pairwiseMatchessame->identicalTostringContainsInOrder->Subject<String>.containsInOrdercontainsAllInOrder(iterable)->Subject<Iterable>.containsMatchingInOrder(iterable)to compare with conditions other than equals,Subject<Iterable>.containsEqualInOrder(iterable)to compare each index with the equality operator (==).
checksdoes not ship with any type checking matchers for specific types. Instead of, for example,isArgumentErroruseisA<ArgumentError>, and similarythrows<ArgumentError>overthrowsArgumentError.anything. When a condition callback is needed that should accept any value, pass(_) {}.- Specific numeric comparison -
isNegative,isPositive,isZeroand their inverses. UseisLessThan,isGreaterThan,isLessOrEqual, andisGreaterOrEqualwith 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 expectationexpectAsyncandexpectAsyncUntil. Continue to importpackage:test/expect.dartfor 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 definedThese 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 suggestionsAsynchronous 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.