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
Base class for widgets that efficiently propagate information down the tree.
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_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
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
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!