Testing the theme switcher: refactor with confidence
In my previous article, that you can find here, we build a tiny application whose only purpose was to switch the theme by pressing two different buttons. However, the code was far from perfect, since we were injecting the same instance of our themeBloc
to many widgets where the themeBloc
had no business, just to be able to make our app work, breaking many good software design principles.
As we can see in the graphic, our ThemeSwitcherApp
and our ThemeSelectorPage
need the same themeBlock
instance, but our three hierarchy has the HomePage
in between. How can we solve this problem?
InheritedWidget is what we are looking for
This is what we can find in the Flutter documentation about InheritedWidget
:
Base class for widgets that efficiently propagate information down the tree.
What InheritedWidget
will allow us to do is share the same instance of themeBloc
without having to inject it in the HomePage
, making our code cleaner, decoupled and more maintainable. Our tree would look something like this now:
… but I am not confident of making the changes without tests
Besides the problems with the code, what I had before was a functional app that works. Our users would be happy with the functionality, so I don’t want to proceed to make changes without verifying that everything is still working as expected. Therefore, we need tests!
Flutter provides three levels of testing:
- Unit tests for testing particular classes or APIs.
- Widget tests for testing Flutter widgets in isolation.
- Integration tests for testing flows/paths of the app as a whole.
Since we already have a Flutter app that works, let’s start doing integration tests, so we can avoid manual regressions.
In order to run integration tests, we will need to create a new folder at the root of our project named test_driver.
In there, we will include two files: app.dart
and app_test.dart
, that looks like this:
Since integration tests will run our app in an emulator/device, we will need a driver, in this case the FlutterDriver
, to drive those interactions. Our test cases are defined inside app_test.dart
, and what we do is to find the button that opens the ThemeSelectorPage
, and make sure that when we tap on the buttons the theme is switched.
In order to run the tests, we will run this command on our terminal:
> flutter drive --target=test_driver/app.dart
Here you can see the results:
Another piece that is crucial to our application, and that we want to make sure we do not break, is our ThemeBloc
. However, to test the ThemeBloc
we will use unit tests instead, since what we want to verify it works is its API declaration.
To write unit tests, Flutter already provides a test
folder; we just need to make sure that we import the test
package, and we are set to go:
With these suite of unit tests, now I am confident the my ThemeBloc
behaves as expected, so I could move pieces around with confident. These tests can be run from the IDE, and they will produce a nice green/red simple report in the IDE’s UI.
Lastly, the last piece that I want to test is my ThemeSelectorPage
widget. Why? Well, I want to make sure that, no matter what, that widget contains my two buttons that toggle the theme between light and dark. In order to do this, I will run widget tests. These tests will be included also inside the test
folder that Flutter gives you by default:
With all these tests, now I can introduce my InheritedWidget
and refactor confidence.
Introducing the InheritedWidget
We are going to create a new file named theme_switcher.dart
:
As we can see there, we passed the instance of ThemeBloc
that we want to share in the with the lower layers of our widget tree, and we just need to override the updateShouldNotifiy(...)
method whenever we change the instance of themeBloc
.
With this, now we can modify main.dart
to include our brand new ThemeSwitcher
, like this:
No magic here; our ThemeSwitcher
Inherited Widget integrates in our tree as any other widget. However, pay attention to one key detail: HomePage
does not require an instance of themeBloc
anymore! That’s how we decouple our pieces; now we can go to the HomePage
and remove it:
Finally, our last step will be to consume our ThemeSwitcher
inside our ThemeSelectorPage
, since themeBloc
does not need to be injected anymore. We will use the static function of
that we created to help us use our inherited widget:
With all these changes applied, it is time to run our tests again to make sure everything is still working as expected, the way it worked before we make all the changes, and…
All the tests are passing!! We have modified the internals of our app to help us grow in the future, improving the quality, while having a way to verify that everything still works as expected. Even more, with our suite test, now we can keep adding new features, and we will have an automated way of running regression without any human interaction.
If you are interested in the entire code base, you can find all the changes here: https://github.com/jorgecoca/theme_switcher/tree/testing
I hope you liked it!
You can follow me on https://twitter.com/jcocaramos and see more code here on my public Github https://github.com/jorgecoca