Flutter: Dynamically Creating and Using Android Emulator for Integration Tests
Enhancing testing efficiency is an important task that often requires balancing the quality and speed of test execution. In a previous article, we looked at methods of ensuring the isolation of each integration test, guaranteeing they don’t influence one another. Today, we will focus on setting up our CI environment to prepare for parallel testing. This article will be particularly valuable for SDET specialists and Flutter developers who want to deepen their understanding of testing and ensure reliable coverage of their projects with UI tests.
Recently, I was faced with improving the quality of an application, which included running mock integration tests at the merge requests stage to verify recently written code. For this purpose, it was decided to use Android emulators and MacMini.
However, the standard command flutter emulators --launch EmulatorName
wasn't suitable for this task as multiple CI jobs could try to use the same emulator simultaneously. Therefore, it was necessary to implement a scenario where each CI job uses its emulator.
‘’The examples below are written using GitLab CI, but they can be easily adapted to other CI platforms.’’
Let’s start by writing a bash script for creating the emulator:
#!/bin/bash
CI_JOB_ID="$1"
echo "Creating emulator with name: ${CI_JOB_ID}"
flutter emulators --create --name ${CI_JOB_ID}
echo "Launching emulator with name: ${CI_JOB_ID}"
flutter emulators --launch ${CI_JOB_ID}
sleep 30
In this script:
CI_JOB_ID="$1"
: We accept the CI job ID as the script input.flutter emulators --create --name ${CI_JOB_ID}
: The command creates a new emulator with a name equal to the CI job ID.flutter emulators --launch ${CI_JOB_ID}
: We launch the created emulator.sleep 30
: The command provides a 30-second pause, usually enough for the emulator to load and start.
Next, we’ll write a script that will wait for the emulator to load fully:
#!/bin/bash
EMULATOR="$1"
echo "Using emulator: ${EMULATOR}"
adb -s "$EMULATOR" wait-for-device
BOOT_COMPLETE=$(adb -s "$EMULATOR" shell getprop sys.boot_completed | tr -d '\r')
echo "Waiting for emulator to boot (this can take several minutes)"
while [ "$BOOT_COMPLETE" != "1" ]; do
sleep 5
echo "$(date +%T) waiting for emulator to boot..."
BOOT_COMPLETE=$(adb -s "$EMULATOR" shell getprop sys.boot_completed | tr -d '\r')
done
adb -s "$EMULATOR" shell input keyevent 82
echo 'Emulator is fully booted and ready for use.'
This script uses adb
(Android Debug Bridge), a command-line tool that allows you to interact with a device or emulator. adb -s "$EMULATOR" wait-for-device
waits until the device/emulator becomes available. Then in a loop, we check if the device loading is completed (getprop sys.boot_completed
). If the loading is not completed (BOOT_COMPLETE != "1"
), we wait another 5 seconds and check again. After the emulator is fully loaded, we send it a keyboard event (adb -s "$EMULATOR" shell input keyevent 82
) that unlocks the emulator screen.
Finally, let’s look at an example of a CI script:
integration_test:
before_script:
- bash create_emulator.sh ${CI_JOB_ID}
- EMULATOR=$(adb devices | grep emulator | tail -n 1 | awk '{print $1}')
- echo $EMULATOR > ../../emulator.txt
- EMULATOR_PORT=$(echo ${EMULATOR} | awk -F- '{print $2}')
- adb connect localhost:${EMULATOR_PORT}
- bash ../../common/testing/avo_integration_tests/lib/helpers/wait_for_emulator_boot.sh "$EMULATOR"
script:
- flutter test integration_test_folder/integration_test_file_name.dart -d ${EMULATOR}
after_script:
- EMULATOR=$(cat emulator.txt)
- adb -s ${EMULATOR} emu kill
- cd ~/.android/avd
- rm -rf ${CI_JOB_ID}.*
Here:
- In
before_script
, we create and start the emulator and then wait for it to load fully. - In
script
, we run the integration test on the created emulator. - In
after_script
, we terminate the emulator and delete it.rm -rf ${CI_JOB_ID}.*
removes all files related to this emulator.
This way, we’ve figured out how to create and use a separate Android emulator for each CI job. This will allow us to perform tests in parallel without worrying that one test run may affect another.
Testing is essential to Flutter development, and there is always something new to learn and apply in practice. I hope this article will be a starting point for those who want to enhance testing in their team.