At Stuart, development moves at a fast pace.
The mobile team are currently rewriting the courier app and, as a result, the QA team has rewritten the integration test suite with a different testing framework, the Flutter integration test framework.
We started by taking a look at something we are currently proud of: our tests for iOS and Android. Unfortunately, these tests have a significant problem.
“Flutter applications run on a single Isolate (Flutter doesn’t have threads, but Isolates) and the test execution is done in a separate Isolate that communicates with the application via a driver”.
This article will explain why this is an issue and how to get around it.
What is needed
Perhaps stating the obvious, but what is needed is to mock the data or information the app requires.
Mock objects are simulated objects that mimic the behaviour of real objects in controlled ways — Wikipedia.org
Mocking inside integration tests
An alternative could be MockWebServer from Square.
MockWebServer works as a standalone mockserver and we can’t use the repository pattern with it. This behaviour forces us to mock the repositories plus maintain the MockWebServer — this is a lot of responsibility to maintain and will possibly lead to tightly-coupled tests which are brittle.
Also MockWebServer expects plain hardcoded strings or json files, which in our case could be a problem to scale.
Another alternative could be something similar to that we do currently with iOS tests: set some expectations before launching the app, as the architecture of the system under test is pretty similar.
The very last alternative we thought is to create a different
main.dart where we can pass as many arguments as we want and, at the end, do something very similar as in iOS without the need of arguments/environments. This solution seemed ‘hacky’ and was also discarded.
Finally we came up with Mockito. Depending on this will help us perform the required mocking for our tests . Luckily, Flutter has extensive support for Mockito so using it is easy.
What’s blocking our mocking?
To mock each repository is a necessity because each test needs a concrete behaviour which the app should comply with. This behaviour also could change during test runtime.
Flutter Driver cannot contain any package or reference to the main app, like Mockito accessing the repositories (remember how Flutter UI testing works — the app is running in one Isolate and the test code in another; interaction done via Flutter Driver). As a result, it is impossible to Mock in the app code and the test code setting the expectations.
To avoid this “problem” we could just add all required mock dependencies for each test inside the corresponding instrumented app. But we shouldn’t expect to modify the app behaviour for every test suite. This case is also a show-stopper since we want to do end-to-end integration tests.
The solution should come with something that would let us interact in some way with the app Isolate at any time. We need to communicate between the two Isolates to be able to modify the behaviour of the app during the test runtime.
Every test expects one or more repositories to be mocked. We do not need to mock every repository, just what is needed for every test. The app Isolate is responsible for this, so all mocked repositories are going the be done by the app.
Communicating between both Isolates is a must-have.
In this case we will use the DataHandler to require the app Isolate to mock our needs during the tests execution.
“Al lío!” (“Let’s get started!”)
We will suppose mock dependencies are solved following the mock dependencies using Mockito and also the structure for Flutter integration testing. Then is as easy as adding the DataHandler
typedef in the enable Flutter Driver Extension for the instrumented app:
This is just adding the
DataHandler inside the Flutter Driver that provides the capability to ‘send’ messages from the test Isolate to a listener we have on the app Isolate.
We also need to allow the test Isolate to request behaviour on the app isolate.
This means sending messages the to the app Isolate like so:
In this case we will try to mock an authentication success:
The driver is the one that sends the message and the DataHandler is the one listening in the app Isolate. Depending on the message received, we will be able to mock that a certain request returns a certain value.
The logic for understanding the message is required inside the DataHandler. This could be, as in this example, a string. Then a switch is used to cover all the different situations:
From this last piece of code we should clarify the “MockHelper”, from
MockHelper.mockLoggedInSuccess();. This is a simple class that contains the required mocks:
And so the magic occurs! ✨
The DataHandler will be called every time the test calls the
driver.requestData, so every time the app Isolate will act as required by the test!
The journey continues
This is our solution and we hope this helps anyone with the same needs.
We are still working on it and expect it can be improved upon. The plan is to reach the same test coverage we have for the actual courier mobile application. After this — who knows? Maybe there is another show-stopper on our path…the challenge continues!
Like what you see? Join us, we’re hiring. 🚀
Thanks to Victor Vargas, Rafael Visa, Sergi Castellsagué Millán, and Sean Handley.