Assertions in Dart and Flutter tests: an (almost) ultimate cheat sheet
An ultimate cheat sheet for assertions in Dart and Flutter tests with many details explained!
≈ 13 minutes readTests 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:
We’ll see the following output:
Here is the full signature of the expect()
method:
expect()
accepts an optional reason
that can be added to the output. For this test:
The output is:
expect()
also accepts an optional skip
that can be either true
or a String
:
This test succeeds with the following output:
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.
Keep reading to learn about the variety of available matchers.
Matcher equals
In the example below, we pass 0
as a matcher
parameter:
In such a case, when the value is passed, an equals
matcher is used implicitly. It is equivalent to:
It is probably the most commonly used matcher, explicitly or implicitly.
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:
equals
matcher fails this test:
With the following output:
It is a good idea to override the .toString()
method to make the output more meaningful. For this improved Result
class implementation:
The test output changes to:
To make it pass, the Result
class has to override the operator ==
, for example like this:
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:
Fails with the following output:
But this test passes:
Interesting observation regarding const
. This test also passes:
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:
This test also passes:
But this test still fails:
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:
And this test passes:
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
The isA<T>
matcher helps verify a variable type:
This test fails with the following output:
Predefined type matchers
There are a couple of more focused type matchers: isList
and isMap
. These tests pass:
Custom type matcher
It is very easy to create your focused type matcher using TypeMatcher
class:
That’s it; now it can be used in tests:
Error matchers
Error type matchers
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:
The hasLength
matcher follows the same principle and calls .length
getter on the passed value:
When the value does not have a .length
getter:
The test fails:
Content matchers
The contains
matcher has different logic depending on the value it is applied to.
For a String
it means substring matching:
For a Map
it means the map has the key:
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
:
The isIn
matcher is the opposite to the contains
matcher:
String matchers
In addition to the collection matchers above that work for String
, there is a couple of matchers more.
Content matchers
startsWith
, endsWith
matchers check String
content along the edges:
matches
The matches
matcher can either accept another String
:
or a RegExp
:
Iterable matchers
In addition to the collection matchers above that work for List
and Set
, there are a few more matchers.
every & any
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:
The actual Iterable
can have additional elements.
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
isZero
, isNonZero
, isPositive
, isNonPositive
, isNegative
, isNonNegative
matchers all check how the actual value is related to 0
:
Range matchers
inInclusiveRange
, inExclusiveRange
matchers check if the actual num
value is in the range:
Comparable matchers
Matchers greaterThan
, greaterThanOrEqualTo
, lessThan
, lessThanOrEqualTo
use operator ==,operator <
, and operator >
to compare expected and actual values:
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:
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.
A test with a predicate
matcher:
It gives the following output:
It can be improved with predicate
matcher description
parameter. This test:
prints:
While a test with an equals
matcher:
gives more information about the expected result with less code:
Always prefer using focused matchers when available.
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:
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:
Now it can be used in any of these ways:
Notice that hasValue
matcher can accept both 0
and equals(0)
matcher. In fact, it can accept any other matcher:
In case of a failing test:
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:
The output prints errors from the first failed matcher:
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:
Even though hasLength
matcher fails, the overall test passes.
isNot
The isNot
matcher calls the inner matcher and inverts its matching result:
Remembering them all
All mentioned matchers are provided by the matcher package.
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.
Having import 'package:flutter_test/flutter_test.dart';
or import 'package:test/test.dart';
implicitly gives access to all matchers from the matcher
package. However, importing it explicitly and assigning it a meaningful name, for example match
, allows using code completion after typing match.
:
Afterword
conclusion
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 on Invertase 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.