Uploaded by Jainam Bhavasar

Flutter Test Guide

advertisement
Image by pch.vector on Freepik, edited with GoDaddy Studio
Flutter Test Guide
Preface
Dear reader and Flutter enthusiast,
Welcome to this guide about testing with a focus on Flutter. From my experience, testing is
an underrated and undervalued task. Many people don’t see benefits in testing and many
programmers prefer developing features instead of writing tests. That’s why I decided to
publish a compact guide about testing with Flutter. After studying this ebook, you’ll have
theoretical knowledge about testing and lots of working code examples so that you can
write better tests in the future or start writing tests at all.
This ebook is for you if you…
want to have an all-in-one guide about testing Flutter applications
have started developing Flutter apps just recently
have no or limited theoretical knowledge about testing
want to start writing good unit and widget tests for your Flutter apps
want to improve your testing skills
This guide contains many code examples which can also be found on GitHub. They are free
and open-source, so don't hesitate to study and use them if you like.
The covered contents include the following:
▶ Testing in software projects
General knowledge about testing, naming schemes, code coverage, test types, best practices
▶ Unit tests
Hands-on example how to write unit tests in a Flutter application with source code
▶ Widget tests
Hands-on example how to write widget tests in a Flutter application with source code
▶ Dependency mocking
Technique to reduce testing down to the parts which you really want to test with source
code
▶ Measuring code coverage
Finding untested code parts by measuring code coverage
➤ Testing is no rocket science! Everybody can do it with a little help. And this ebook might
be all the help you need to get started!
2
Flutter Test Guide
About the author
I have been a professional software developer for over 10 years and I started coding at the
age of 15. Since then, I worked for many companies, used many different technologies and
programming languages, and gathered a lot of experience in developing software for various
industries.
My first Flutter project was in 2019 and as the Flutter framework matured, my knowledge
increased as well. Today, Flutter is my first choice when it comes to developing crossplatform applications. I also am a content creator on Medium where I publish lots of coding
tutorials from the Flutter world. All my code produced for any articles is available on GitHub.
You can find more information about me with the following links
 My Medium profile
 My GitHub profile
 Some freebies, curated lists, and contact information
2
3
Flutter Test Guide
PREFACE ........................................................................................................................................................... 2
ABOUT THE AUTHOR ........................................................................................................................................ 3
TESTING IN SOFTWARE PROJECTS .................................................................................................................... 5
WHY TEST AT ALL?
TYPES OF TESTS
NAMING
STRATEGIES
CODE COVERAGE
BEST PRACTICES
5
5
6
7
7
8
UNIT TESTS ....................................................................................................................................................... 9
FLUTTER TESTING CONSTRAINTS
CLUSTERING TESTS
SETUP AND TEARDOWN
CODE EXAMPLE
TESTING THE INC() METHOD
TESTING THE DEC() METHOD
CONCLUSION
9
10
11
11
11
12
13
WIDGET TESTS ................................................................................................................................................ 14
THE APP TO TEST
WRITING THE TESTS
CONCLUSION
14
15
17
DEPENDENCY MOCKING ................................................................................................................................. 18
THE CLASS TO TEST
GENERATING THE MOCK
USING THE MOCK IN TESTS
DEALING WITH LEGACY APPS
MOCKITO CHEAT SHEET
CONCLUSION
18
19
20
20
21
22
MEASURING CODE COVERAGE ....................................................................................................................... 23
THE TEST SUBJECT
THE UNIT TESTS
COLLECT COVERAGE INFORMATION
SHOW COVERED LINES IN THE EDITOR
SHOW COVERAGE INFORMATION PER FILE
CONCLUSION
23
24
24
25
26
26
CLOSING WORDS ............................................................................................................................................ 27
4
Flutter Test Guide
Testing in software projects
Testing is an essential task in every software project (or at least it should be one). In this
section, we’ll talk about some basic knowledge that every developer should have about
testing. It’s about best practices, naming conventions, testing strategies, and what type of
test is used in which situation.
Why test at all?
As soon as you compile and run your application, you have made the first test even if you
didn’t intend to. So if you ever hear somebody say they don’t do any testing at all, you know
better now. Every software developer does testing. The question is just how much they
invest into it.
Testing won’t guarantee bug-free software, but testing will increase your chances to find
errors in your code. More tests don’t necessarily lead to more findings. Testing quality and
strategy are essential factors for a flawlessly running app. For example, it doesn’t make
sense (and it’s also not possible) to test any imaginable input of a text field. It‘s better to
focus on edge cases like an empty string, numbers, special characters, and so on. The
context is important and the developer has to identify these edge cases when writing tests.
Types of tests
Tests can roughly be divided into two types: manual and automatic tests. Both have their
pros and cons that we’ll look into now.
👨 Manual tests
A manual test is any test that involves human interaction. Compiling the code and running
the app is the most basic one. Even complete use cases (launch the app, open the login page,
insert email and password, press button, …) are sometimes tested repeatedly by dedicated
testers. This is very expensive but sometimes it’s the reality because of a lack of knowledge,
environmental constraints, or management errors.
💻 Automated tests
Automated tests consist of unit tests, widget tests, and integration tests in the Flutter
environment. They are usually executed by a build server and don’t require additional
interaction. This reduces maintenance costs and overall testing time. In general, you should
aim to automate as many tests as possible.
But what type of automated tests are useful in what context?
Use unit tests to test a single function, method, or class. Use widget tests to test a single
widget, and use integration tests to test an entire app or large parts of it. The following table
compares different types in various categories.
5
Flutter Test Guide
Speed
Dependencies
Maintenance cost
Code covered
Unit
Very fast
Very little
Very low
Very little
Widget
Fast
Little
Normal
Little
Integration
Normal
Normal
High
Normal
Manual
Very slow Very much
Very high
Very much
Comparison of different test types in various categories
When you write tests according to best practices, you‘ll end up with lots of unit tests, fewer
widget tests, even fewer integration tests, and hopefully no manual tests. The impact
grows with every category but so do maintenance costs and required dependencies. A good
balance between these categories is required and with more experience, you’ll figure it out
eventually.
Naming
Naming tests is an important task to write maintainable code. A good naming convention
allows other developers to quickly recognize the purpose of a test. Therefore, a lot of
patterns have been created over the years. I’ll introduce the most common ones. You can
use them or come up with your own variant. Being consistent and understandable is the key.
Given When Then
The Given When Then pattern is used a lot in Behavior-Driven Development. It defines
preconditions (Given), an action (When), and a result (Then). This leads to very long, but
easily understandable test names.
▶ GivenDatabaseIsUp_WhenUserInputIsValid_ThenUserIsCreated
▶ given_input_is_valid_when_button_is_clicked_then_value_is_converted_to_currency
▶ givenFilterIsNotEmptyWhenFilterButtonIsClickedThenMatchingItemsAreReturned
When Then
The When Then pattern is similar to the Given When Then pattern but omits any
preconditions. Names will be shorter, but most likely not as understandable.
▶ WhenUserInputIsValid_ThenUserIsCreated
▶ when_button_is_clicked_then_value_is_converted_to_currency
▶ whenFilterButtonIsClickedThenMatchingItemsAreReturned
Should
The Should pattern gives a hint of what a test should check for.
▶ ShouldCreateUserInDatabase
▶ should_convert_to_currency
▶ shouldReturnMatchingItems
Should When
The Should When pattern indicates the outcome of a test by including a condition. It’s more
verbose than the Should approach and tends to be more understandable.
6
Flutter Test Guide
▶ ShouldCreateUserInDatabaseWhenInputIsValid
▶ should_convert_to_currency_when_input_is_valid
▶ shouldReturnMatchingItemsWhenConditionIsFulfilled
Verify
The Verify approach tells a user what the outcome of a test should be.
▶ VerifyCreateUserInDatabase
▶ verify_input_convertion_to_currency
▶ verifyMatchingItems
Descriptive sentence
Use a descriptive sentence to indicate what a test should do. Use camel-case notation or
separate words with underscores.
▶ CreateUserInDatabase
▶ convert_input_to_currency
▶ findMatchingItems
Strategies
There are two common testing strategies: white-box testing and black-box testing.
A white-box test involves the analysis of the source code to write a test. In general, unit
tests are white-box tests because they test small entities like classes or functions. White-box
tests are usually written by developers only and are very low-level.
During black-box testing, the developer doesn’t care about the source code. These tests
define inputs and outcomes. The input is passed on to the testing entity and any result will
be compared to the specified outcome which was defined beforehand. Integration and
manual or system tests work mostly according to this pattern. Black-box tests are more highlevel and can be defined by stakeholders, requirements engineers, or any person on a
project team. It doesn’t need to be a developer.
Code coverage
Code coverage tools can measure which code parts have been executed by tests. This helps
us finding untested areas. It doesn’t tell us if the test is good or not. It is also not guaranteed
that a covered part is bug-free. Code coverage is a helper to identify untested code parts,
nothing more and nothing less.
Some people believe that high coverage of the code base improves quality. But from my
experience, I can tell that metrics like 80% overall coverage don’t lead to fewer bugs. It
sometimes just makes the developer write a test to fulfill the coverage goal.
Let’s have a look at a code example to learn how we can extract unit tests. The code snippet
is below:
7
Flutter Test Guide
double _calcPrice(String item, int amount, String subType) {
if (item.isEmpty) {
throw Exception("no item provided");
}
switch (item) {
case "cpu":
return 99.99 * amount;
case "graphics":
if (subType.contains("amd") || subType.contains("nvidia")) {
return 199.99 * amount;
}
if (subType.contains("intel")) return 99.99 * amount;
throw Exception("unknown graphics card");
default:
throw Exception("unknown item");
}
}
This function calculates a price based on given arguments.
First, we handle branch coverage. This means our tests cover every branch. For if-conditions,
we need two tests. For switch statements, we need one test per case branch plus one for the
default branch.
Next, we examine statement coverage. This means every statement is executed at least
once. With our previously found tests, all statements are already covered. So we don’t need
to add any further tests.
Lastly, we focus on condition coverage. This means every condition is checked for every
possible outcome. Our previous tests already do that, with one exception: The line if
(subType.contains("amd") || subType.contains("nvidia")) needs three tests:
▶ subType.contains("amd") is true, subType.contains("nvidia") is false
▶ subType.contains("amd") is false, subType.contains("nvidia") is true
▶ subType.contains("amd") and subType.contains("nvidia") are both false
This is how you identify tests to fulfill coverage. As you can see, some tests are redundant
and apply to multiple coverage categories. But there are still some tests missing. For
example, it‘s possible to pass a negative number as amount argument which leads to a
negative price. This bug can‘t be prevented by just having 100% overall test coverage. Make
sure to think of edge cases!
Best practices
Here are some best practices to write good unit tests.
 Keep your tests short One should always see what a test does at first glance. If scrolling
is necessary or many method calls are involved, it gets messy.
 Make your test independent of other tests A test should always work in the same way.
So make sure to set up data properly (and even reset them after the test if needed).
 Write tests while writing code Don’t skip the tests when you start. It is more likely that
you will end up with untestable code or that you won’t test everything if you don’t do
it while writing the code parts.
8
Flutter Test Guide
Unit Tests
We now know about the basics of testing in software products. In this chapter, we will focus
on writing test cases as unit tests and how to structure them. You’ll learn the basics of unit
testing in Flutter applications. It will give you a compact overview to start with. A link to all
sources is provided at the end.
This is the class we are going to write unit tests for.
class CounterService {
int _counter = 0;
int get counter => _counter;
final int? maxCounterValue;
CounterService({this.maxCounterValue});
/// Increases the counter value if [maxCounterValue] has not been reached yet.
CounterService inc() {
if (maxCounterValue == null || _counter < maxCounterValue!) _counter++;
return this;
}
/// Decreases the counter value if [counter] is bigger than zero.
CounterService dec() {
if (_counter > 0) _counter--;
return this;
}
}
This class manipulates a counter value when the methods are called. There are some
conditions to be considered and a maximum value which the counter can’t exceed.
Flutter testing constraints
The Flutter test runner expects all files containing tests to end with the suffix _test.dart. In
addition, they must be located in the test folder of a Flutter project structure. They are
executed with the command flutter test or with special buttons in various IDEs and code
editors.
Flutter test support in Visual Studio Code. The red arrows indicate options to run, debug, and find tests in a
Flutter project.
9
Flutter Test Guide
Project structure with a test folder and a test file in Visual Studio Code
Clustering tests
The easiest way to cluster tests is by putting them in separate files. It is a common practice
to have a test file for every code file. You’ll notice that in the upcoming example which uses
a class counter_service.dart and its corresponding test class counter_service_test.dart.
The test package of the Flutter SDK offers the possibility to define test groups. A test group
is a set of tests with a common description which can be run together. Groups can also be
nested if needed. It looks like this:
void main() {
group("group1", () {
test("test1", () {});
test("test2", () {});
group("inner group", () {
test("test3", () {});
});
});
}
10
Flutter Test Guide
There is the additional option to define tags. A tag is a custom identifier for a test that can
be interpreted by the test runner. You can include or exclude specific tags from your test run
which is useful if you are testing platform-specific code like different operating systems (iOS,
Android) or browsers (Chrome, Firefox).
➤ Documentation and example code for tags can be found on GitHub.
Setup and teardown
Sometimes, tests need a setup like creating objects or copying files to specific locations. For
these cases, there are also methods in the test package.
▶ setUp()
▶ setUpAll()
▶ tearDown()
▶ tearDownAll()
➡ runs once before every test
➡ runs once before any test of the suite or group
➡ runs once after every test
➡ runs once after all tests of the suite or group
They all expect a function as an argument and can be used at the top level or in groups (see
the previous section).
Code example
Let’s assume your class to test is called counter_service.dart and sits under the lib folder in
your project. Your matching test class will be called counter_service_test.dart and will be in
the test folder structure. If you follow this convention the Flutter test runner can find and
execute your tests. It can be triggered by the command flutter test while you are in the
root folder of your project.
Testing the inc() method
First of all, we test the inc() method. Our goal is to execute every code branch which results
in the following three tests.
 Validate that the counter is increased if there is no maxCounterValue set.
 Validate that the counter is increased if maxCounterValue is not reached yet.
 Validate that the counter is not increased if maxCounterValue is reached.
test("Verify counter can be increased", () {
final _sut = CounterService();
_sut.inc().inc().inc();
expect(_sut.counter, equals(3));
});
test("Verify counter can be increased if below max value", () {
final _sut = CounterService(maxCounterValue: 2);
_sut.inc().inc();
expect(_sut.counter, equals(2));
});
test("Verify counter cannot be increased above max value", () {
final _sut = CounterService(maxCounterValue: 0);
_sut.inc();
expect(_sut.counter, equals(0));
});
11
Flutter Test Guide
Every test creates an instance of the class being tested (System Under Test, that’s why I call
the variable _sut), manipulates data, and checks the results. Verification of results is done by
the expect function, which takes the actual value and a Matcher to evaluate the outcome.
Here are some examples of how to work with matchers.
test("Some matcher examples", () {
// type check
expect(CounterService(), isA<CounterService>());
// null check
expect(null, isNull);
expect("null", isNotNull);
// bool check
expect(3 == 3, isTrue);
expect(3 != 3, isFalse);
// equality check
expect("1", equals("1"));
// sign check
expect(-1, isNegative);
expect(1, isPositive);
expect(0, isNonNegative);
expect(0, isNonPositive);
// list checks
expect([], isEmpty);
expect([2], isNotEmpty);
});
Testing the dec() method
This method is simpler as we only need two tests to cover all possible outcomes.
 Validate that the counter can be decreased
 Validate that the counter cannot be decreased below zero
test("Verify counter can be decreased", () {
// Arrange
final _sut = CounterService();
_sut.inc().inc().inc();
expect(_sut.counter, equals(3));
// Act
_sut.dec();
// Assert
expect(_sut.counter, equals(2));
});
test("Verify counter cannot be decreased below zero", () {
final _sut = CounterService();
_sut.dec();
expect(_sut.counter, equals(0));
});
As you can see every test follows the Arrange-Act-Assert pattern. It helps to structure the tests
and is one of the most common strategies to write test code.
To execute the tests, run the command flutter test. The test results will be displayed in the
console window. Many IDEs or editors support running tests as well.
12
Flutter Test Guide
Conclusion
In this chapter, we focused on unit testing in Flutter applications. You learned about test
organization, writing tests, and executing them with Visual Studio Code or the command
line. In combination with the code examples, you should be able to create tests for your
application.
➤ You can find the source code on GitHub.
13
Flutter Test Guide
Widget tests
In this section, we’ll focus on widget tests. This means that a part of the app is rendered
during the tests and you can describe test steps to copy the behavior of a real user. The
Flutter SDK offers methods to simulate a complete workflow.
The app to test
I created a simple app that needs some widget tests. Here’s a demo.
Demo Flutter application to be tested
The app contains a Scaffold with a static AppBar, a description Text, a ListView, and a
FloatingActionButton. When you click on the button, a new row is inserted into the ListView
with a random number between 0 and 10, the header text is updated, and a SnackBar is
shown. You can also delete entries with a long press. A simple tap shows a confirmation of
what row you clicked. This is what we are going to test now.
14
Flutter Test Guide
Writing the tests
As the app workflow is pretty simple, we’ll write just one test to fulfill all requirements. You
can, of course, split your if it makes sense. Good test organization is essential in larger apps,
so take your time and think about what’s reasonable.
The tests use action steps simulating user input or actions and verification steps verifying if
the outcome matches our expectations. Our test will do the following:







Verify that there are already 3 data items present
Tap the FloatingActionButton
Verify that a new data item has been added
Tap the 3rd item
Verify that the SnackBar shows the same data as in the list
Long press the 2nd item
Verify if the correct item has been deleted
The test skeleton for widget tests is similar to unit tests that you have seen in the previous
chapter but has an additional WidgetTester argument. It always looks like this
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Test description', (WidgetTester tester) async {
});
}
WidgetTester
The WidgetTester is the central unit performing actions during a test. For example, there are
methods to
▶ write into TextField widgets → enterText
▶ tap widgets → tap
▶ trigger animations and rendering → pump, pumpAndSettle
Finding widgets
To find widgets, use the Finder class. It has many methods to find exactly what you are
looking for. Some examples are
▶ find.byType
▶ find.byKey
▶ find.text
15
Flutter Test Guide
Auto-completion options of the Finder class in VS Code
Verification
For verification, use the expect library like in regular unit tests. A typical expect statement can
look like this: expect(find.byText(”test”), findsOneWidget); Look for one or more specific
widgets and assert it.
Putting it all together
The general workflow in widget tests is always the same. First, you use a Finder to identify a
widget you want to interact with. Then, you pass it to the WidgetTester to perform an action
and trigger animation as well as rendering. In the end, you verify the outcome via the expect
class.
Here’s a test example. The entire app source code will be linked at the end.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_widget_test/list_widget.dart';
import 'package:flutter_widget_test/main.dart';
void main() {
testWidgets('Test app workflow', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that 3 data items exist
// Here we look for 3 ListWidget elements which are the widgets that are
// used in the ListView.
expect(find.byType(ListWidget), findsNWidgets(3));
// Tap the FloatingActionButton
// Here we use a Type Finder to find our item. Works great if there is only
// one element of a type present.
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
// Verify that a new row has been added
// We again look for 4 ListWidget elements. Furthermore, we look for a
// SnackBar and the matching text that is displayed when a new data item is
// added.
expect(find.byType(ListWidget), findsNWidgets(4));
16
Flutter Test Guide
expect(find.byType(SnackBar), findsOneWidget);
expect(find.text("item 4 added"), findsOneWidget);
// Tap 3rd item in the list
// We use a Key Finder to identify the ListWidget we want to tap. Keys are
// always unique and a good way to find the exact widget. The pumpAndSettle
// is required because the previous SnackBar auto-dismisses after 1 second
// and we need to wait for that to happen.
await tester.tap(find.byKey(const Key("ListWidget2")));
await tester.pumpAndSettle(const Duration(milliseconds: 1250));
// Verify shown data is correct
// We look for the SnackBar and a certain text.
expect(find.byType(SnackBar), findsOneWidget);
expect(find.text("item 3 pressed"), findsOneWidget);
// Long press 2nd item
// Again a Key Finder and a pumpAndSettle to wait for the previous SnackBar.
await tester.longPress(find.byKey(const Key("ListWidget1")));
await tester.pumpAndSettle(const Duration(milliseconds: 1250));
// Verify pressed item was deleted
// Finding widgets by type, by key, or by text. There are many possibilities.
expect(find.byType(ListWidget), findsNWidgets(3));
expect(find.byKey(const Key("ListWidget0")), findsOneWidget);
expect(find.byKey(const Key("ListWidget1")), findsOneWidget);
expect(find.byKey(const Key("ListWidget2")), findsOneWidget);
expect(find.byKey(const Key("ListWidget3")), findsNothing);
expect(find.byType(SnackBar), findsOneWidget);
expect(find.text("item 2 deleted"), findsOneWidget);
});
}
Conclusion
In this chapter, we had a look at Widget testing in Flutter applications. You learned about the
WidgetTester, how to perform actions, and how to verify the outcome with code examples.
You should be capable of constructing a useful testing procedure for your apps now.
➤ You can find the source code on GitHub.
17
Flutter Test Guide
Dependency mocking
During testing, there will always be a moment when you need a mock. This will save you
from setting up complex environments for a test as it allows you to focus on the parts you
want to test. Assume you got a logger in multiple places of your application that writes into a
database. Sure enough, you have tests for your logger to ensure its correct functionality. But
when you test e. g. your login mechanism, you don’t care about the logger. Here’s when a
mock comes in handy. It handles the calls to the logger and reacts the way it was configured.
In this section, we are looking at the Mockito package for Flutter apps. It allows you to create
mocks so that any dependencies of your classes don’t need to be handled for testing.
The class to test
This is the DataService class we are going to write tests for. It takes a DataStore object as a
constructor argument. The DataStore can be anything from a remote database over local
device storage to a simple Map<string, dynamic>() object that stores the values. We don’t
want to deal with this dependency, so we are going to mock it in our test.
import 'data_store.dart';
class DataService {
final DataStore store;
DataService(this.store);
dynamic get(String identifier) {
return store.get(identifier);
}
void add(String identifier, dynamic data) {
store.add(identifier, data);
}
void update(String identifier, dynamic data) {
store.update(identifier, data);
}
void remove(String identifier) {
store.remove(identifier);
}
}
Our class to be tested
18
Flutter Test Guide
Generating the mock
The build runner tool of Dart helps to create mocks for every class we want. We add it to our
Flutter app via the command flutter pub add build_runner. Afterward, we specify classes
for which a mock should be generated. We add an annotation to the main() method of our
empty test class (see code below).
@GenerateMocks([DataStore])
void main() {}
Annotation to create mocks by the build runner tool
And in the end, we call the build runner with flutter pub run build_runner build. It will
create a new class containing all mocks specified in the array of the annotation.
Build runner output
Generated class with mocks
19
Flutter Test Guide
Using the mock in tests
To test the get() method, we need to set up the mock. Otherwise, an exception will be
thrown, because since Dart 2.12/Flutter 2 and Null Safety there isn’t a common default
value like null which could be returned.
To specify the behavior, we use the when() and thenAnswer() methods:
test("Verify get returns values", () {
var storeMock = MockDataStore();
var service = DataService(storeMock);
when(storeMock.get(any)).thenAnswer((_) => "I am a value!");
expect("I am a value!", service.get("dummy"));
verify(storeMock.get(any)).called(1);
});
Mock with setup
Whenever the get() method of the mock is called, it answers with a fixed value which we
can verify afterwards. Additionally, we can ask the mock if the get() method was called to
be more confident.
Dealing with legacy apps
❗ The following hint is not the recommended behavior and should only be used for legacy
apps without Null Safety support.
If you want your mocks to return null when no setup is available, then replace your
annotation
@GenerateMocks([DataStore])
with
@GenerateMocks([],customMocks: [MockSpec<DataStore>(returnNullOnMissingStub: true)])
Your test looks like this
test("Verify get returns values", () {
var storeMock = MockDataStore();
var service = DataService(storeMock);
service.get("dummy");
verify(storeMock.get(any)).called(1);
});
Mock with a default return value of null
20
Flutter Test Guide
Mockito Cheat Sheet
Here is a cheat sheet for the Mockito package used to mock dependencies in the previous
chapter. It shows various options to set up mocks and verify interactions.
test("Some tests with mockito", () async {
//////////////////////////////////
// --- Define return values --- //
//////////////////////////////////
// Returns a Future without a value
when(mock.addAsync(any, any))
.thenAnswer((realInvocation) => Future.value());
await sut.addAsync("2", "2");
verify(mock.addAsync(any, any)).called(1);
// Returns a Future with a value
when(mock.getAsync(any)).thenAnswer((realInvocation) => Future.value("42"));
expect(await sut.getAsync("2"), "42");
verify(mock.getAsync(any)).called(1);
//////////////////////////////
// --- Throw exceptions --- //
//////////////////////////////
// Throws exception
when(mock.removeAsync(any)).thenThrow(Exception());
when(mock.remove(any)).thenThrow(Exception());
expect(() => sut.remove("1"), throwsA(isA<Exception>()));
expect(() async => await sut.removeAsync("1"), throwsA(isA<Exception>()));
verify(mock.remove(any)).called(1);
verify(mock.removeAsync(any)).called(1);
/////////////////////////////////////
// --- Using argument matchers --- //
/////////////////////////////////////
// Matches on any argument
when(mock.get(any)).thenAnswer((realInvocation) => "42");
expect(mock.get(null) == "42", isTrue);
// Matches on any argument of type string
when(mock.get(argThat(isA<String>()))).thenAnswer((realInvocation) => "42");
expect(mock.get("") == "42", isTrue);
// Matches on any named argument
when(mock.updateAsync(any, data: anyNamed("data")))
.thenAnswer((realInvocation) => Future.value());
await mock.updateAsync("dummy", data: 123);
verify(mock.updateAsync(any, data: anyNamed("data"))).called(1);
/////////////////////////////////
// --- Capturing arguments --- //
/////////////////////////////////
// Captures the arguments
await mock.updateAsync("test", data: 321);
expect(
verify(mock.updateAsync(captureAny, data: captureAnyNamed("data")))
.captured,
["test", 321]);
// Captures the arguments according to a condition
await mock.updateAsync("test", data: 321);
await mock.updateAsync("not a test", data: 322);
await mock.updateAsync("testme", data: 323);
expect(
verify(mock.updateAsync(captureThat(startsWith("t")),
data: captureAnyNamed("data")))
.captured,
["test", 321, "testme", 323]);
//////////////////////////
21
Flutter Test Guide
// --- Verification --- //
//////////////////////////
clearInteractions(mock);
// Verifies the number of calls
mock.get("111");
mock.get("222");
verify(mock.get(any)).called(2);
mock.get("111");
mock.get("222");
verify(mock.get(any)).called(lessThan(3));
// Verifies a mock wasn't called with the given arguments
verifyNever(mock.get("123"));
// Verifies the order of call
mock.get("get");
mock.add("add", "add");
mock.get("get");
verifyInOrder([mock.get(any), mock.add(any, any), mock.get(any)]);
// Verifies that there weren't any more calls after the last verification
verifyNoMoreInteractions(mock);
reset(mock);
// Verifies that the mock wasn't called at all
verifyZeroInteractions(mock);
///////////////////////
// --- Resetting --- //
///////////////////////
// Removes all stubs, clears captured calls
reset(mock);
// Clears captured calls
clearInteractions(mock);
});
Final tips
▶ Always use thenAnswer(), never use thenReturn()  Explanation
▶ If you verify a mock, the invocation counter is reset!
▶ If there are multiple matching setups, the last setup wins!
Conclusion
With this cheat sheet, you have all the tools needed to use mocking with Mockito.
➤ You can find the source code on GitHub.
22
Flutter Test Guide
Measuring code coverage
Now, let me show you how you can measure your code coverage and see the covered and
uncovered lines in your Flutter app. We are going to use Visual Studio Code and two free
extensions from the Visual Studio Marketplace: Flutter Coverage and Coverage Gutters.
Flutter Coverage extension for Visual Studio Code to show code coverage for every file
Coverage Gutters extension for Visual Studio Code to show covered and uncovered lines
The test subject
I created a simple class called login_service.dart. It allows logging in, logging out, and
checking the current state (logged in or not). It’s very basic and has flaws (like hard-coded
user name and password) but will be enough to demonstrate how coverage works. The class
is shown below.
class LoginService {
final String _expectedPassword = "abc123!?=";
final String _expectedUser = "superUser";
bool _isLoggedIn = false;
void login(String? user, String? password) {
if (_isLoggedIn) throw Exception("Please log out first");
if (user != _expectedUser) throw Exception("Wrong user name");
if (password != _expectedPassword) throw Exception("Wrong password");
_isLoggedIn = true;
}
void logout() {
_isLoggedIn = false;
}
bool isLoggedIn() {
return _isLoggedIn;
}
}
23
Flutter Test Guide
The unit tests
Furthermore, we have some unit tests to cover all possible statements of the LoginService:
void main() {
test("Verify failed login with wrong user", () {
var _sut = LoginService();
expect(() => _sut.login("user", "abc123!?="), throwsException);
expect(_sut.isLoggedIn(), isFalse);
});
test("Verify failed login with wrong password", () {
var _sut = LoginService();
expect(() => _sut.login("superUser", "password"), throwsException);
expect(_sut.isLoggedIn(), isFalse);
});
test("Verify logout before login", () {
var _sut = LoginService();
_sut.login("superUser", "abc123!?=");
expect(_sut.isLoggedIn(), isTrue);
expect(() => _sut.login("superUser", "abc123!?="), throwsException);
expect(_sut.isLoggedIn(), isTrue);
});
test("Verify successful login", () {
var _sut = LoginService();
_sut.login("superUser", "abc123!?=");
expect(_sut.isLoggedIn(), isTrue);
});
test("Verify logout works", () {
var _sut = LoginService();
expect(_sut.isLoggedIn(), isFalse);
_sut.login("superUser", "abc123!?=");
expect(_sut.isLoggedIn(), isTrue);
_sut.logout();
expect(_sut.isLoggedIn(), isFalse);
_sut.logout();
expect(_sut.isLoggedIn(), isFalse);
});
}
Collect coverage information
Flutter has a built-in command to collect coverage information while executing all tests. So
you just need to open a shell of your choice, navigate to the project root folder, and execute
the following command:
flutter test --coverage
This will execute all tests in your project and create a file lcov.info containing the coverage
information. The extensions you installed earlier rely on the file to visualize the information.
24
Flutter Test Guide
Show covered lines in the editor
After the coverage information has been collected open an already tested file (in this
example login_service.dart). You should see something like this.
Covered lines by tests in Visual Studio Code
A green line means there is a test executing this code line while a red line means that there
is no test executing this code line. This class has a coverage of 100% as all unmarked lines
(like field declarations) are not measured by the Flutter tool.
❗ Be aware that the colors don’t indicate if a test passes or not. They just tell you if there is
any test for a line.
If you can’t see any colorings check the status bar and enable the coverage gutters.
Coverage display is not enabled, click to enable
Coverage display is enabled, click to disable
25
Flutter Test Guide
Show coverage information per file
In bigger projects, you’d like to have an overview of the files having sufficient test coverage
and those which don’t. To solve this problem you can use the Flutter Coverage extension. It
shows the coverage percentage of every dart file in your project. Go to your Test Explorer in
VS Code and open the Flutter Coverage drawer. You should see something like this.
Test coverage per file with Flutter Coverage in VS Code
The coverage of the login_service.dart class is 100% as we have concluded before.
Especially if your project gets bigger and bigger this view will be quite handy to identify
insufficiently tested classes.
Conclusion
With the extensions Flutter Coverage and Coverage Gutters, you are more likely to notice
missing tests in your code base. They can play an important role in your testing process and
will hopefully lead to fewer bugs in your application.
➤ You can find the source code on GitHub.
26
Flutter Test Guide
Closing words
Thank you for reading this test guide. I’ve put a lot of work into it and there will be future
versions with additional content. I hope you gained valuable insights and new knowledge on
your path to becoming a (better) Flutter developer. If you have any questions regarding a
specific topic or feedback, don’t hesitate to contact me.
Also, check my Gumroad page for other free stuff, ebooks, and digital content, and be sure to
follow me on Medium to not miss any new articles and tutorials about Flutter.
Image from freepik
27
Download