Adding testing to a Flutter app
I speak with a lot of developers about Flutter, as it is a really interesting approach to dealing with the need to ship beautiful apps on both iOS and Android. One of the questions I received frequently in these conversations was how do we go about testing a Flutter app. So, in the interests of figuring out a viable approach, I decided to add testing to a non-trivial app on GitHub.
Jake Gough put together a neat little flutter app called drawapp
that does pretty much what it says on the tin. The only issue is that at the time it had no tests. Which meant this was a perfect play pen to figure out how hard it is to add tests after the fact, and a workable approach to do so.
This is going to be the tale of how I grew the test coverage of drawapp
from nothing to pretty good. And I’m going to tell this tale in the order of the git commits, as that seems oddly logical for this time of the afternoon.
The place to start is the test
directory. This directory contains a single file, widget_test.dart
, which has a single test labelled “Counter increments smoke test”. Running flutter test
at the command line at this point will show us a failing test.
As much as a single failing test feels like a bad starting point, this test shows us the path forward. It starts the app, clicks on a button, and checks the resulting state of the user interface. That’s a pretty solid base. I went ahead and changed up widget_test.dart
to click on the paint brush floating action button (also known as a FAB), and confirm it made the change I thought it was making. Except the test failed.
The reason my test failed is interesting. I assumed that the mini FABs that are shown by clicking on the main FAB aren’t in the widget tree when they are hidden. Not true, they were present, just that they are exceedingly small.
This required that I change the underlying application, which you can see the diff for here. The main trick I used here was to convert the hidden mini-FABs into null
, and then filter the list for null
s before handing the list back to Flutter for rendering.
At this point, with a single working test, it was time to see what was being tested, and what wasn’t. The flutter test framework sports a neat option, in the form of flutter test --coverage
which generates a report in coverage/lcov.info
. This coverage report can then be inspected in most IDEs and editors. I used Ryan Luker’s Coverage Gutters, which can be installed into Visual Studio Code to give guidance on where to add testing next. There is equivalent functionality for IntelliJ IDEA and Android Studio available.
With coverage in place, I started getting serious with the tests. I even correctly labelled the first UI test in this change, along with adding a second test to make sure clicking the main Floating Action Button a second time closed the mini-FABs again.
Getting serious, adding lint checks
One of the parts that I really like about Flutter, and the underlying Dart programming language, is that the team have put together tooling for adding lint checks and code formatting. There is a package on pub.dartlang.org that packages up a collection of lints that reflect a good starting point for writing readable Dart code called pedantic
.
As you can see in this commit, I installed the pedantic pub package, and then added a couple of additional lints. Oh, and I re-formatted the whole codebase with flutter format .
I strongly recommend teams building flutter apps to add in appropriate lints to their analysis_options.yaml
configuration, along with enforcing code formatting at commit time, as this makes code reviews less about code stylistic issues and more about the semantics of the commit.
The next interesting change were in these two commits. The functionality we are testing here is making sure that the values returned from the Dialog
s represent the value entered by the user into the dialog. This required the creation of a very small inline MaterialApp
along with a StatefulWidget
to wrap the dialog in to capture the returned value.
At this point the coverage tooling told an interesting story. We have coverage for the opening and closing of the mini FABs, and we have tested the pop-up dialogs. What we haven’t tested is the drawing itself. Just the most important functionality of the app.
Breaking down a wall, one BLoC at a time
The reason I couldn’t easily test the logic of drawing in drawapp
is the state pertaining to the captured strokes was smeared throughout DrawPageState
. While intermixing display logic and application state is a quick way to get started, the mixing of concerns makes testing hard.
An approach to escaping out of this bind is to use the BLoC pattern. For a quick refresher on the BLoC pattern, please see Filip Hracek’s write up on building reactive mobile apps in Flutter. The BLoC pattern can be a tad bit intimidating at first glance, but it does make testing business logic a whole lot easier by separating the business logic from presentation.
For implementing the Business Logic Component (BLoC) pattern, I used Didier Boelens’ Reactive Programming — Streams — BLoC article as the starting point. His BlocBase
and BlocProvider
proved instrumental in getting things up and running.
Even with these resources at hand it took me several attempts to get the BLoC implementation working. The first step was modelling the state using built_value
. I used this to turn the implicit state in the original drawapp
into an explicit model that includes colours, locations, and strokes. You can see it in this commit.
The author of built_value
, David Morgan, has written a good series of articles covering built_collection for immutable collections, built_value for immutable object models, serialisation, and finally building a chat app with these tools.
With an explicit data model in hand, we can go about building the BLoC to represent the actions of adding strokes to the page, changing the paint colour, and updating the brush size. There are some tricky sections in this implementation, but the upside is that we can now test our business logic completely independent of the Flutter UI for drawapp
. You can see a snapshot of the development effort that shows a BLoC implementation with tests, before integrating it into drawapp
proper.
Improving Streams with RxDart
The other key piece of infrastructure that made this transformation possible was RxDart. It wasn’t until very late in development, when I was busy debugging race conditions while flying from Sydney to Perth, that I realised that I really needed a way to gate between the asynchronous world of Streams back to the synchronous world to make it possible to show the current state of brush width when opening a dialog.
The fix, shown in this commit, was to utilise RxDart’s BehaviorSubject
to make it effortless to synchronously access the most recently published value in a stream. This is required to be able to show the user of the app the current size of the paint brush in the pop up dialog to update brush size.
Hopefully this quick tour shows you a workable approach to adding testing to a Flutter app. I haven’t hit 100% coverage, but the coverage we do have here gives us a more stable base when thinking about adding new features. How about we add a colour that traces out a rainbow effect while painting? Sure!