Assertions in Dart and Flutter Tests
Assertions in Dart and Flutter Tests
io /blog/assertions-in-dart-and-flutter-tests-an-ultimate-cheat-sheet
Anna Leushchenko
6 December, 2022
Tests are essential for ensuring any software quality. Whether you are creating unit, widget, or integration
tests for Flutter applications, the end goal of any test is asserting that the reality matches the
expectations. Here is an ultimate cheat sheet for assertions in Dart and Flutter tests with many details
explained!
Cheat sheet
Flutter Tests
Each of the items in this cheat sheet is discussed in greater detail in this article.
Check the official website for the overall approach to testing Flutter apps.
Before you start, you can read all tests in the following Zapp.run project.
Expect
expect() is the main assertion function. Let’s take a look at this test:
where result is a value that would typically come from software under test.
Here, expect() ensures that the result is 0. If it is different, it will throw the TestFailure exception,
leading to test failure.
Additionally, expect() prints the description of the problem to the output. For example, for this test:
Expected: <0>
Actual: <1>
expect() accepts an optional reason that can be added to the output. For this test:
Expected: <0>
Actual: <1>
Result should be 0!
expect() also accepts an optional skip that can be either true or a String:
Attention! The usage of the skip parameter does not skip the entire test, but only the expect() call it is
applied to.
Next, we will focus on the matcher parameter of the expect() method and explore what values it can
accept.
Matcher
Matcher is an instance that validates that the value satisfies some expectation based on the matcher
type. It is either a child of the Matcher base class or a value. Matcher is also responsible for providing a
meaningful description of a mismatch in case of test failure.
Matcher equals
In such a case, when the value is passed, an equals matcher is used implicitly. It is equivalent to:
The equals matcher uses the equality operator to perform the comparison. By default, classes in Dart
are compared “by reference” and not “by value”. Thus, if applied to custom objects like this Result class
here:
class Result {
Result(this.value);
final int value;
}
It is a good idea to override the .toString() method to make the output more meaningful. For this
improved Result class implementation:
class Result {
Result(this.value);
@override
String toString() => 'Result{value: $value}';
}
To make it pass, the Result class has to override the operator ==, for example like this:
class Result {
Result(this.value);
@override
String toString() => 'Result{value: $value}';
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Result &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
}
Equality matchers
Apart from an equals matcher that compares objects with operator ==, and is used implicitly when an
expected value is passed instead of a matcher; there are more explicit equality matchers.
same
The same matcher makes sure expected and actual results are the same instance. This test:
Because 1 is a const and only one instance exists in memory. The same applies when custom classes
declare const constructors and instances are created with const modifiers. If the Result class is
updated to declare a const constructor:
class Result {
const Result(this.value);
because without using const, two different instances of Result are created.
null matchers
The next pair of matchers is quite simple: isNull and isNotNull check result nullability.
Fails with:
Expected: null
Actual: <0>
bool matchers
The next pair of equality matchers is self-explanatory: isTrue and isFalse. These tests pass:
anything
The anything matcher matches any value. It is used in any from mockito package or any<T> from
mocktail, which we’ll probably discuss later. However, it’s not a commonly used matcher in client
application tests.
Type matchers
isA
There are a couple of more focused type matchers: isList and isMap. These tests pass:
It is very easy to create your focused type matcher using TypeMatcher class:
Error-type matchers are based on the TypeMatcher class from the example above, as they check for the
error type: isArgumentError, isException, isNoSuchMethodError, isUnimplementedError, etc.
throwsA
throwsA is a matcher that ensures the method call resulted in an error. If the method call is supposed to
throw, it’s unsafe to call it in the test body. Instead, it should be called inside the expect() call. throwsA
matcher accepts another matcher that validates the error, for example, one of the error matchers above:
Collection matchers
By “collection” I mean String, Iterable, and Map.
Size matchers
The pair of isEmpty and isNotEmpty matchers call respective .isEmpty or .isNotEmpty getters on a
result, and expect them to return true:
When used with the type that does not have isEmpty or isNotEmpty methods:
isEmpty and isNotEmpty matchers fail the test with the following output:
Expected: non-empty
Actual: <0>
NoSuchMethodError: Class 'int' has no instance getter 'isNotEmpty'.
Receiver: 0
Tried calling: isNotEmpty
The hasLength matcher follows the same principle and calls .length getter on the passed value:
Content matchers
The contains matcher has different logic depending on the value it is applied to.
And for Iterable it means there is an element matching the matcher that is passed inside contains
matcher. In this test first a predicate matcher is used, and then an implicit equals:
String matchers
In addition to the collection matchers above that work for String, there is a couple of matchers more.
Content matchers
or a RegExp:
Iterable matchers
In addition to the collection matchers above that work for List and Set, there are a few more matchers.
Matchers everyElement and anyElement verify that all or some elements satisfy a matcher or equal to a
value they accepted as a parameter:
Content matchers
Matchers containsAll and containsAllInOrder verify that the Iterable passed as a parameter is a
subset of the actual Iterable, optionally verifying items’ order:
Matchers orderedEquals and unorderedEquals check that the actual Iterable is of the same length
and contains the same elements as the passed Iterable, optionally verifying items’ order:
Map matchers
In addition to collection matchers that work for Map, there are just a couple more.
The containsValue matcher checks if the actual .containsValue method returns true:
The containsPair matcher checks both pair’s key and value, where the value can be another matcher:
Numeric matchers
Zero-oriented matchers
Range matchers
inInclusiveRange, inExclusiveRange matchers check if the actual num value is in the range:
Comparable matchers
They can be applied not only to numeric values but also to custom classes. To use them in our Result
class, it has to be improved with operator < and operator > implementations:
class Result {
const Result(this.value);
...
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Result &&
runtimeType == other.runtimeType &&
value == other.value;
As you see, I am comparing Result objects by the inner value field. Now, these tests also pass:
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:
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.
prints:
gives more information about the expected result with less code:
Expected: <0>
Actual: <1>
Custom matchers
If you did not find a matcher that satisfies your requirements, you could 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:
@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. 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:
Notice that hasValue matcher can accept both 0 and equals(0) matcher. In fact, it can accept any other
matcher:
The output contains the feature name and description passed to CustomMatcher constructor:
Matcher operators
allOf
The allOf matcher allows combining multiple matchers and ensures all of them are satisfied. It can be
used with an array of matchers or with up to 7 individual matchers:
In case of failure:
Expected: (an object with length of <1> and an object with value property of <1>)
Actual: Result:<Result{result: 0}>
Which: has no length property
anyOf
The anyOf matcher also accepts an array of matchers or up to 7 individual matchers and ensures at least
one of them is satisfied:
isNot
The isNot matcher calls the inner matcher and inverts its matching result:
With so many matchers, it may take a lot of work to remember them all. Let alone the cheat sheet you
can have at any time; it’ll be much easier if they all belonged to a single class, for example, Matcher. In
this case, we could type in Matcher., trigger code completion suggestions, and pick the suitable matcher
from the list. It is unlikely ever to become true, but there is a way around which gives almost the same
result.
There is still a lot to cover in this topic, including asynchronous matchers, Flutter widget matchers, etc.
Check out the sequel post about assertions in Dart and Flutter tests!
Stay tuned for more updates and exciting news that we will share in the future. Follow us onInvertase
Twitter, Linkedin, and Youtube, and subscribe to our monthly newsletter to stay up-to-date. You may also
join our Discord to have an instant conversation with us.
Anna Leushchenko
Flutter GDE