Android Emulator Development, Anbox, & Waydroid

Beyond the Basics: Solving Flaky Tests with Intelligent ADB Scripting in Multi-Emulator Setups

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: Taming the Chaos of Flaky Tests

In the demanding world of Android application development, automated testing is a cornerstone of a robust CI/CD pipeline. However, as test suites grow and deployment targets expand across multiple emulators or devices (including AVDs, Anbox, and Waydroid), developers frequently encounter a insidious problem: flaky tests. These are tests that pass or fail inconsistently without any code changes, often attributed to timing issues, resource contention, or state synchronization problems.

This article dives deep into how intelligent ADB (Android Debug Bridge) scripting can be leveraged to meticulously orchestrate tests across multiple Android emulator instances, significantly reducing flakiness and improving the reliability of your automated testing infrastructure. We will move beyond basic ADB commands to craft resilient scripts that can dynamically manage devices, ensure proper state, and provide detailed diagnostic feedback.

The Multi-Emulator Predicament: Why Flakiness Flourishes

Running tests concurrently or sequentially across numerous emulators introduces a layer of complexity that often manifests as non-deterministic test failures. Understanding the root causes is the first step towards mitigation:

Common Causes of Instability

  • Timing and Race Conditions: Tests often assume a certain state is achieved instantly (e.g., app fully loaded, network request completed). In a multi-emulator environment, variations in boot times, network conditions, or background processes can lead to tests starting before the system is ready, causing failures.
  • Resource Contention: Running multiple emulators simultaneously demands significant CPU, RAM, and disk I/O. Overburdened host machines can introduce delays, lead to UI freezes, or even crash emulators, making tests unreliable.
  • State Mismatch: If tests don’t explicitly reset the emulator’s state or the application’s data between runs, residual data from a previous test or instance can interfere with subsequent tests, leading to unexpected outcomes.
  • Network Latency: While emulators typically use the host’s network, variations in network setup or reliance on external services can introduce delays that affect tests sensitive to response times.

Setting the Stage: Your Multi-Emulator Environment

Before diving into scripting, ensure your environment is configured for multi-emulator testing:

Android Emulator Setup

Utilize the Android SDK’s AVD Manager to create multiple Android Virtual Devices. For robust testing, consider different API levels and device configurations to simulate real-world diversity.

Anbox and Waydroid Integration (Brief Mention)

For Linux users, Anbox and Waydroid offer alternative ways to run Android environments directly on your host system. While their integration with ADB is generally seamless, specific setup or resource allocation might vary. The ADB scripting principles discussed here remain universally applicable regardless of the emulator technology.

Mastering ADB for Multi-Device Orchestration

The core of intelligent multi-emulator testing lies in ADB’s ability to target specific devices and query their state. The -s <serial> flag is paramount.

Targeting Specific Devices

To list all connected devices (physical or emulated):

adb devices

This command typically outputs something like:

List of devices attached
emulator-5554 device
emulator-5556 device
emulator-5558 device

To execute a command on a specific emulator, use adb -s <serial> <command>:

adb -s emulator-5554 shell getprop ro.boot.qemu.avd_name

This allows you to identify which AVD corresponds to which serial, crucial for targeted actions.

Essential ADB Commands for Scripting

  • adb wait-for-device: Blocks until the device is online. Crucial for ensuring an emulator has finished booting.
  • adb install <path_to_apk>: Installs an application.
  • adb shell pm grant <package> <permission>: Grants runtime permissions.
  • adb shell am instrument -w -r -e class <test_class> <package>/<runner>: Runs Android instrumentation tests (e.g., Espresso).
  • adb logcat -d: Dumps the entire log buffer. Useful for capturing logs on failure.
  • adb pull <remote_path> <local_path>: Copies files from the device to the host.
  • adb shell screencap -p <path_on_device>: Takes a screenshot.

Crafting Robust ADB Automation Scripts

Let’s build a script that addresses the flakiness common in multi-emulator setups.

1. Dynamic Device Detection and Iteration

Your script shouldn’t hardcode device serials. Instead, it should discover them dynamically:

#!/bin/bash

DEVICES=$(adb devices | grep emulator | awk '{print $1}')
if [ -z "$DEVICES" ]; then
echo "No emulators found. Please start your emulators first."
exit 1
fi

for DEVICE in $DEVICES; do
echo "Processing device: $DEVICE"
# ... script logic for each device ...
done

2. Ensuring Device Readiness (Boot Completion)

adb wait-for-device ensures the ADB daemon is ready, but not necessarily that Android has fully booted. Checking sys.boot_completed is essential:

adb -s $DEVICE wait-for-device

boot_completed=$(adb -s $DEVICE shell getprop sys.boot_completed 2>/dev/null)
while [[ "$boot_completed" != "1" ]]; do
echo "Device $DEVICE not fully booted, waiting..."
sleep 10
boot_completed=$(adb -s $DEVICE shell getprop sys.boot_completed 2>/dev/null)
done
echo "Device $DEVICE is ready."

3. Implementing Idempotent Operations and State Reset

To avoid flaky tests caused by previous states, always start clean:

  • Uninstall before install: Ensures a fresh app installation.
  • Clear app data: Use adb shell pm clear <package> if you need to retain the app but reset its data.
# Uninstall previous app version (to ensure clean slate)
echo "Uninstalling $APP_PACKAGE from $DEVICE (if exists)..."
adb -s "$DEVICE" uninstall "$APP_PACKAGE" > /dev/null 2>&1
sleep 2 # Give it a moment

4. Intelligent Test Execution and Reporting

After running tests, parse the output and capture diagnostic information on failures. This is critical for debugging flaky tests.

# Run the tests
TEST_OUTPUT_FILE="test_results_${DEVICE}.txt"
adb -s "$DEVICE" shell am instrument -w -r -e class "$TEST_CLASS" "$APP_PACKAGE/$TEST_RUNNER" > "$TEST_OUTPUT_FILE" 2>&1

# Check test results and capture artifacts on failure
if grep -q "FAILURES!!!" "$TEST_OUTPUT_FILE"; then
echo "FAILURES DETECTED on $DEVICE!"
echo "Capturing screenshot and logcat..."
SCREENSHOT_NAME="failure_screenshot_${DEVICE}.png"
LOGCAT_NAME="logcat_${DEVICE}.txt"

adb -s "$DEVICE" shell screencap -p "/sdcard/$SCREENSHOT_NAME"
adb -s "$DEVICE" pull "/sdcard/$SCREENSHOT_NAME" "./artifacts/$SCREENSHOT_NAME" > /dev/null 2>&1
adb -s "$DEVICE" shell rm "/sdcard/$SCREENSHOT_NAME" # Clean up on device

adb -s "$DEVICE" logcat -d > "./artifacts/$LOGCAT_NAME"

echo "Artifacts saved: ./artifacts/$SCREENSHOT_NAME, ./artifacts/$LOGCAT_NAME"
else
echo "Tests passed on $DEVICE."
fi

A Comprehensive Multi-Emulator Test Script Example

Here’s a complete bash script demonstrating these principles. Create a directory named artifacts in the same location as your script to store output.

#!/bin/bash

# Configuration
APK_PATH="./app-debug.apk" # Path to your application APK
APP_PACKAGE="com.example.yourapp" # Your application's package name
TEST_RUNNER="androidx.test.runner.AndroidJUnitRunner" # Your test runner
TEST_CLASS="com.example.yourapp.YourTestSuite" # Specific test class or leave blank for all tests

echo "--- Starting Multi-Emulator Test Orchestration ---"

# Create an artifacts directory if it doesn't exist
mkdir -p artifacts

# Get list of all running emulator devices
DEVICES=$(adb devices | grep emulator | awk '{print $1}')

if [ -z "$DEVICES" ]; then
echo "No emulators found. Please start your emulators first."
exit 1
fi

echo "Found devices: $DEVICES"

for DEVICE in $DEVICES; do
echo ""
echo "========================================="
echo "Processing device: $DEVICE"
echo "========================================="

# 1. Wait for device to be online and fully booted
echo "Waiting for device $DEVICE to be online..."
adb -s "$DEVICE" wait-for-device

echo "Checking boot completion for $DEVICE..."
boot_completed=$(adb -s "$DEVICE" shell getprop sys.boot_completed 2>/dev/null)
while [[ "$boot_completed" != "1" ]]; do
echo "Device $DEVICE not fully booted, waiting..."
sleep 10
boot_completed=$(adb -s "$DEVICE" shell getprop sys.boot_completed 2>/dev/null)
done
echo "Device $DEVICE is ready."

# 2. Uninstall previous app version (to ensure clean slate)
echo "Uninstalling $APP_PACKAGE from $DEVICE (if exists)..."
adb -s "$DEVICE" uninstall "$APP_PACKAGE" > /dev/null 2>&1
sleep 2 # Give it a moment

# 3. Install the application
echo "Installing $APK_PATH on $DEVICE..."
adb -s "$DEVICE" install "$APK_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: Failed to install $APK_PATH on $DEVICE. Skipping tests for this device."
continue
fi
sleep 5 # Give app a moment to settle

# 4. Grant necessary permissions (example: storage, network)
echo "Granting permissions for $APP_PACKAGE on $DEVICE..."
adb -s "$DEVICE" shell pm grant "$APP_PACKAGE" android.permission.READ_EXTERNAL_STORAGE
adb -s "$DEVICE" shell pm grant "$APP_PACKAGE" android.permission.WRITE_EXTERNAL_STORAGE
# Add other permissions as needed for your app
sleep 2

# 5. Run the tests
echo "Running tests on $DEVICE..."
TEST_OUTPUT_FILE="artifacts/test_results_${DEVICE}.txt"
adb -s "$DEVICE" shell am instrument -w -r -e class "$TEST_CLASS" "$APP_PACKAGE/$TEST_RUNNER" > "$TEST_OUTPUT_FILE" 2>&1

# 6. Check test results and capture artifacts on failure
if grep -q "FAILURES!!!" "$TEST_OUTPUT_FILE"; then
echo "FAILURES DETECTED on $DEVICE!"
echo "Capturing screenshot and logcat..."
SCREENSHOT_NAME="failure_screenshot_${DEVICE}.png"
LOGCAT_NAME="logcat_${DEVICE}.txt"

adb -s "$DEVICE" shell screencap -p "/sdcard/$SCREENSHOT_NAME"
adb -s "$DEVICE" pull "/sdcard/$SCREENSHOT_NAME" "./artifacts/$SCREENSHOT_NAME" > /dev/null 2>&1
adb -s "$DEVICE" shell rm "/sdcard/$SCREENSHOT_NAME" # Clean up on device

adb -s "$DEVICE" logcat -d > "./artifacts/$LOGCAT_NAME"

echo "Artifacts saved: ./artifacts/$SCREENSHOT_NAME, ./artifacts/$LOGCAT_NAME"
else
echo "Tests passed on $DEVICE."
fi

# 7. Clean up (optional: uninstall app after tests)
echo "Uninstalling $APP_PACKAGE from $DEVICE..."
adb -s "$DEVICE" uninstall "$APP_PACKAGE" > /dev/null 2>&1
sleep 2
done

echo ""
echo "--- Multi-Emulator Test Orchestration Complete ---"
echo "Review test_results_*.txt files and artifacts/ directory for details."

Advanced Techniques (Briefly)

For even larger scale testing, consider:

  • Parallel Execution: Use tools like xargs -P <num_processes> with your script or background shell jobs (&) to run tests on multiple devices concurrently. This requires careful management of output and error handling.
  • Dynamic Device Allocation: In cloud-based testing setups, you might dynamically provision and de-provision emulators, integrating your ADB scripts with cloud APIs.

Conclusion

Flaky tests are a significant impediment to developer productivity and confidence in automated test suites. By adopting intelligent ADB scripting, particularly in multi-emulator environments, you can implement robust orchestration strategies that account for device readiness, manage state, and provide actionable diagnostic feedback. The examples provided serve as a solid foundation for building your own resilient and efficient Android testing pipeline, moving you beyond the basics to a more stable and trustworthy CI/CD process.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner