Assertions in Dart and Flutter tests: universal and custom matchers

This is the part of the ultimate cheat sheet dedicated to:

  • universal matcher predicate,
  • custom matcher.

In this series:

Universal matcher

Generally speaking, most types of checks a developer might ever need to perform in expect() methods can be expressed with a single matcher - predicate. It accepts a predicate - a Function with one parameter that returns bool, where you can decide if the parameter matches your expectations. For example:

test('expect: predicate βœ…', () {
final result = Result(0);
expect(result, predicate((e) => e is Result && e.value == 0));
expect(result, predicate<Result>((result) => result.value == 0));
});

Depending on the type of required check, predicate might be exactly the matcher you need. But there is a bunch of more focused matchers which provide more readable code and output. Let’s compare.

A test with a predicate matcher:

test('expect: predicate ❌', () {
final result = 1;
expect(result, predicate((e) => e == 0));
});

gives the following output:

Expected: satisfies function
Actual: <1>

It can be improved with predicate matcher description parameter. This test:

test('expect: predicate ❌', () {
final result = 1;
expect(result, predicate((e) => e == 0, 'Result should be 0!'));
});

prints:

Expected: Result should be 0!
Actual: <1>

While a test with an equals matcher:

test('expect: equals ❌', () {
final result = 1;
expect(result, equals(0));
});

gives more information about the expected result with less code:

Expected: <0>
Actual: <1>

Always prefer using focused matchers when available.

Custom matchers

If you did not find a matcher that satisfies your requirements, you can create your own matcher.

For example, let’s create a matcher that validates the value field. For that, we need a child of CustomMatcher class:

class HasValue extends CustomMatcher {
HasValue(Object? valueOrMatcher)
: super(
'an object with value field of',
'value field',
valueOrMatcher,
);

@override
Object? featureValueOf(dynamic actual) => actual.value;
}

The HasValue class extends CustomMatcher and accepts one parameter, which can be a value or another matcher. It calls the parent constructor with the feature name and description, which will be used in the output if the test fails.

It also overrides the featureValueOf method that attempts to get value property of the actual object passed to expect(). It is supposed to work with any type that declares the value property, like the Result class created in the expect and matchers part. In case actual does not declare such a property, our featureValueOf implementation will throw, but the base CustomMatcher class calls it inside try / catch bloc and will fail the test gracefully.

To be consistent with common practices of declaring a matcher, let’s also declare a factory method to create our matcher:

Matcher hasValue(Object? valueOrMatcher) => HasValue(valueOrMatcher);

Now it can be used in any of these ways:

test('expect: hasValue βœ…', () {
final result = Result(0);
expect(result, hasValue(0));
expect(result, HasValue(0));
expect(result, hasValue(equals(0)));
});

Notice that hasValue matcher can accept both 0 and equals(0) matcher. In fact, it can accept any other matcher:

test('expect: hasValue βœ…', () {
final result = Result(0);
expect(result, hasValue(isZero));
expect(result, hasValue(lessThan(1)));
});

In case of a failing test:

test('expect: hasValue ❌', () {
final result = Result(1);
expect(result, hasValue(0));
});

The output contains the feature name and description passed to CustomMatcher constructor:

Expected: an object with value property of <0>
Actual: Result:<Result{result: 1}>
Which: has value property with value <1>

Originally published at Invertase blog. Check out their awesome Authors Program!

Hi! πŸ‘‹πŸ» I’m Anna, Google Developer Expert in Flutter from Ukraine πŸ‡ΊπŸ‡¦ Follow me on Twitter, GitHub, YouTube, Medium to get notifications about my latest work.

It’s early 2023, and we in Ukraine are still fighting against russians committing genocide on our lands. If you find this content useful and have a coin to spare, support us with your donations. Stand with Ukraine!

--

--

Anna Leushchenko πŸ‘©β€πŸ’»πŸ’™πŸ“±πŸ‡ΊπŸ‡¦

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator