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 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.

Trying Checks as a Preview

  1. Add a dev_dependency on checks: ^0.2.0.

  2. Replace the existing package:test/test.dart import with package:test/scaffolding.dart.

  3. Add an import to package:checks/checks.dart.

  4. 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.

  5. 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.

expect(actual, expected);
// or maybe

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();

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 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)).
  • 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.

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.

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.