Assertions in Dart and Flutter tests: an ultimate cheat sheet
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!
≈ 10 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
The previous post covered a significant part of checks you can perform in Flutter and Dart tests and explained more than half of this cheat sheet in detail. Here we focus on the bottom half, starting with asynchronous matches.
Before you start, you can read all tests in the following Zapp.run project.
Asynchronous expect
The expectLater()
function is just like expect()
, but returns a Future
that completes when the matcher has finished matching.
While expectLater()
can accept any matcher, it makes sense to pass children of AsyncMatcher
class, which does asynchronous computation.
Future matchers
There are a few matchers to test Future
execution results.
The completes
matcher completes successfully when the Future
completes successfully with any value.
The completion
matcher accepts the matcher to verify the Future
result:
And the throwsA
matcher should be already familiar:
Stream matchers
First, we’ll focus on testing streams with hardcoded values to see the variety of stream matchers. And then, we’ll have a word about testing streams when it’s too late to use the expect()
function.
emits / neverEmits
The emits
matcher checks that the Stream has emitted a value that satisfies a matcher, that emits
has been accepted as a parameter. It may accept the expected value, another matcher that characterizes the expected value, or a predicate function:
The neverEmits
matcher performs the opposite check:
emitsInOrder / emitsInAnyOrder
These matchers ensure a stream has emitted multiple events.
In particular order:
Or in no particular order:
As you see, both accept an array, containing expected values or matchers.
emitsDone
The emitsDone
matcher helps ensure a stream does not emit any more unexpected values:
emitsError
The emitsError
matcher helps ensure a stream has emitted an error and accepts another matcher to verify the exact error:
Testing closed / drained streams
So far we tested streams that contained hardcoded values, which were emitted immediately inside the expect()
function. But imagine, we have to test a stream that was already closed or a stream that has already emitted values we are interested in.
Let’s take a look at this class:
When doWork
method is called, the stream
should emit two values: 0
and 1
. Here is a test that comes to mind for this behavior:
Unfortunately, the expect()
function is called too late, emitted values are already gone, and this test never completes. Instead, expect()
or expectLater()
should be used before doWork()
call.
Unlike using expectLater()
with Future matchers, where it is placed at the end of the test and is awaited, for testing StreamMatcher
, it should be placed before performing the calls that affect stream values. This way, we can catch values as the stream emits them. In such a case, the expectLater()
call should not be awaited, otherwise, the test will not be complete as well.
Flutter widgets matchers
Flutter widget tests use the same expect()
method to verify the actual widget tree content matches expectations. However, the list of matchers is unique to this task.
No / One / N widgets matchers
This group of matchers is the most commonly used in Flutter widget tests.
findsNothing
ensures no widget in the widget tree matches the first parameter of the expect()
method:
The findsWidgets
/findsOneWidget
matchers ensure at least/exactly one widget is present in the tree:
In the example above, findsOneWidget
matcher failed the test because the widget tree contains two widgets with the text '1'
. The test output is:
The findsAtLeastNWidgets
/findsNWidgets
matchers ensure at least/exactly N widgets are present in the widgets tree.
Color matcher
isSameColorAs
helps verify properties of Color
type of any widget:
Parent matchers
isInCard
/isNotInCard
helps to verify that widget has at least one/no Card
widget ancestor:
Error matchers
We all know that the Container
widget cannot accept both color
and decoration
parameters because of the following assert
in its constructor:
If you take a similar approach when implementing your widgets, the throwsAssertionError
matcher can help with testing these assert
s:
Because throwsAssertionError
uses throwsA
matcher under the hood, the same principle applies here: the function that is expected to throw should be called under the expect()
method call.
The throwsFlutterError
matcher verifies that a function throws FlutterError
. Here is an example from the Flutter framework tests:
Accessibility matchers
There is only one pair of accessibility matchers: meetsGuideline
/doesNotMeetGuideline
. These matchers are asynchronous, and thus should be used with the expectLater
assertion function mentioned before.
They accept AccessibilityGuideline
object, which represents the type of performed accessibility check. There are several predefined guidelines to check against:
androidTapTargetGuideline
checks that tappable nodes have a minimum size of 48 by 48 pixels;iOSTapTargetGuideline
checks that tappable nodes have a minimum size of 44 by 44 pixels;textContrastGuideline
provides guidance for text contrast requirements specified by WCAG;labeledTapTargetGuideline
enforces that all nodes with a tap or long press action also have a label.
Your own guidelines can be created by inheriting AccessibilityGuideline
class or creating your own instances of MinimumTapTargetGuideline
, MinimumTextContrastGuideline
, LabeledTapTargetGuideline
.
Let’s take a look at this example:
The GestureDetector
will have a size of 46×46, which is just enough to satisfy the iOSTapTargetGuideline
which requires 44×44 tap area. A similar test that uses androidTapTargetGuideline
, which requires a 48×48 tap area, fails:
with the following output:
Let’s check an example using textContrastGuideline
:
Black text on white background has a good contrast ratio, so this test passes. However, small orange text on a white background is hard to read, and this test fails:
with the following output:
As you see, the output mentions that the expected contrast ratio depends on the font size. In the example above, when no text style was provided in Text
widget, nor in MaterialApp
, the default text size of 14
was applied. Interestingly enough, if the text font is increased, the same test passes because larger texts are easier to read even when the contrast ratio is not perfect:
Golden matchers
Image file matcher
The matchesGoldenFile
matcher allows validating that a Finder
, Future<ui.Image>
, or ui.Image
matches the reference (golden) image file:
Image object matcher
The matchesReferenceImage
matcher allows validating that a Finder
, Future<ui.Image>
, or ui.Image
matches the reference ui.Image
object:
For testing a Finder
, as in the test above, it must match exactly one widget and the rendered image of the first RepaintBoundary
ancestor of the widget is treated as the image for the widget.
Mock invocations and parameters
So far, we have used expect()
function to compare the results of some operations with the expected values. There is another category of checks developers constantly perform: ensuring that some side effect was triggered.
Verify invocations
For that, the verify()
function is used:
This verify()
function comes from the mocktail package. The mockito package provides similar functionality.
For context, here is the definition of MockService
:
The verify()
function ensures that provided method was called at least once. The test above fails because the sideEffect()
method was never called, with the following output:
To ensure certain method was called exactly N times, use the called()
method of verify()
function result:
To ensure certain method was never called, use the verifyNever()
function:
Verify parameters
In the example above, the sideEffect()
method accepts two parameters. In case their values do not matter, use the any()
object that can represent literally any value:
In case of invocation parameters matter, real values can be used instead of any()
:
Similar to equals
matcher, verify
will compare parameters using the equality operator. Thus, for the above test to pass, the Result
class has to have the operator ==
overridden, as was shown before. Otherwise, the test fails with the following output:
In case you are only interested in checking those parameters met some condition, any()
call can accept one of the matchers we looked so closely at before as that
parameter:
This post concludes the topic of assertions available in Dart and Flutter tests.
Follow Anna on social media to get notifications about her latest work!
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.