Flutter: Improving the Speed of Integration Testing.
Greetings, Medium readers. The previous article discussed speeding up Unit and Widget testing in Flutter. Today, I want to continue this discussion by offering solutions and recommendations for accelerating integration tests.
This article will be precious for SDET specialists and Flutter developers who want to deepen their knowledge in testing and ensure reliable coverage of their projects with UI tests.
In the course of the article, we will cover:
- How to speed up the running of mock integration tests.
- How to set up application cleaning between tests.
- Implementation of a mechanism to run tests.
- Implementation of the launch of tests from the desired screen.
Improving the Efficiency of Integration Testing
One of the most noticeable problems when working with integration tests in Flutter is the time required for their execution. As mentioned in the previous article, Flutter requires time to start tests for each file containing tests, as it needs to compile, create a new process, and so on. In the case of integration tests, a new application build can take 30–40 seconds for each run.
To cope with this problem, we’ll again use the trick of running all tests from a single file, which will allow running all tests or any group of test files at the same time as a single test file.
import test_1_test.dart as test_1
import test_2_test.dart as test_2
void main() {
test_1.main();
test_2.main();
}
Nevertheless, such an approach generates a new problem: when running multiple tests from one file, the application’s state is not cleared between tests. As a result, the next test starts from where the previous one ended. To solve this issue, it is necessary to develop a mechanism for cleaning the application between tests.
Setting up and cleaning the application between tests
Another crucial aspect of testing is ensuring a “predictable” application state before each test. Let’s discuss how to prepare and clean the application between tests to guarantee that each test is conducted in a stable and predictable environment.
The following examples will carry more theoretical and educational value and help you understand the general concept and approach to implementing your process. This is because the implementation of all applications varies, and different packages are used in each project.
class IntegrationTestHelper {
Future<void> init() async {
/* You need to do the realization:
- Environment initialization
- Register instance
- Mocks initialization
*/
}
Future<void> dispose() async {
/* You need to do the realization:
- Clearing app states
*/
}
Future<void> runTest({
required Future<Null> Function() run,
required Future<Null> Function() after,
}) async {
try {
await run();
} finally {
await after();
}
}
}
In this helper class, you will need to develop your methods for initialization and cleaning. The comments indicate the main directions to follow in development.
Also provided is an example of the runTest function for running tests. Using the try/catch block in this context is significant to ensure 100% execution of the after block, thereby ensuring the correctness and reliability of your tests.
Implementation of the application cleaning logic for the after block
Let’s extend the functionality of the WidgetTester class using an extension.
extension IntegrationTesterExtention on WidgetTester {
Future<void> performTestCleanUp(
IntegrationTestHelper helper,
IntegrationTestWidgetsFlutterBinding binding,
) async {
await helper.dispose();
binding.reset();
binding.resetEpoch();
}
}
In this function, we:
- Invoke the application state cleanup logic.
- Prepare the IntegrationTestWidgetsFlutterBinding for the next test
Now our test can look like this:
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
final IntegrationTestHelper helper = IntegrationTestHelper();
final StepLogger logger = StepLogger();
tearDown(() async {
await Future<void>.delayed(const Duration(seconds: 1));
});
testWidgets(('Test'), (tester) async {
await helper.runTest(
run: () async {
await helper.init();
// Some test logic
},
after: () async {
await tester.performTestCleanUp(
helper,
binding,
);
},
);
});
The delay in the tearDown block is required to ensure the complete execution of the after block, as in the event of failures in the run block, background processes may require more time to complete. This action will help avoid potential problems with application freezing.
Implementing Test Launch from a Specific Screen
This idea will allow us to further speed up the passage of test scenarios, as we will not have to perform extra steps.
We must modify our performTestCleanUp method with an example of redirection using GoRouter.
Future<void> performTestCleanup(
IntegrationTestHelper helper,
IntegrationTestWidgetsFlutterBinding binding,
String route,
) async {
final appRouter = getIt<GoRouter>();
appRouter.go(route);
await pumpAndSettle(const Duration(seconds: 1));
await helper.dispose();
await getIt.reset();
binding.reset();
binding.resetEpoch();
binding.resetFirstFrameSent();
}
This short guide allows us to:
- Write multiple tests in one file and clean up states between them.
- Run tests from one file, eliminating the need to rebuild the application between tests in different files.
- Start tests from a specific screen, bypassing preconditions.
I hope this article will be helpful in your journey of mastering Flutter and improving testing practices. Remember, the key to effective testing is its applicability and relevance to your project.