loading...

Build a Lightweight Docker Container For Android Testing

fastphat profile image fastphat Updated on ・7 min read

Testing helps us identify any defects or errors that may have been made during development. But it takes time and resources, especially in Android where it requires the installation of many dependencies and a device to perform UI tests.

By using a Docker container, we can build and run tests for multiple feature branches, speeding up the development and increasing productivity.

In this tutorial, we’re going to learn how to build a lightweight Android container to isolate the testing process.

  • No Android Studio/GUI applications required.

  • Android emulator runs on a Docker container.

  • Has ability to cache dependencies for later build.

  • Wipe out everything after the process.

1. Start docker container

The image that we build on top of is: ubuntu:latest

Assuming you have docker installed (if not, please follow this link), you can run this to start docker container:

$ docker run --privileged -dit --name android-container ubuntu
  • privileged: grant permission to launch VM on container.

  • it: interactively execute shell cmd.

  • d: run container in the background.

  • name android-container: container’s name, will use later to attach and commit docker image.

(Optional) To run docker as non-root, the simplest way is adding the current user to group docker

$ sudo groupadd docker // Add group docker if it doesn't already exist
$ sudo gpasswd -a $USER docker // Add current user to group docker
$ newgrp docker // reload (or you can re-login to reload)
$ docker run hello-world // check if it works

2. Install SDK packages

Prerequisites:

Make sure you install the following dependencies. Otherwise, you may notice No such file or directory when installing android SDK or start emulator

Please double-check the redundant dependencies.🙇 (ex: vim — if you’re not a fan)

$ apt update && apt install -y openjdk-8-jdk vim git unzip libglu1 libpulse-dev libasound2 libc6  libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxi6  libxtst6 libnss3 wget

Gradle

Install gradle and gradle-wrapper . We’ll use version 5.4.1 in this tutorial. No worries, it can be updated with build-arg later 😊

Download gradle-5.4.1 to /tmp/gradle-5.4.1 and unzip the content within /opt/gradle

$ wget https://services.gradle.org/distributions/gradle-5.4.1-bin.zip -P /tmp \
&& unzip -d /opt/gradle /tmp/gradle-5.4.1-bin.zip 

Make a new directory /opt/gradlew and install gradle-wrapper there. (The directory names can be anything, but save the files to somewhere that easy to find)

$ mkdir /opt/gradlew \
&& /opt/gradle/gradle-5.4.1/bin/gradle wrapper --gradle-version 5.4.1 --distribution-type all -p /opt/gradlew \
&& /opt/gradle/gradle-5.4.1/bin/gradle wrapper -p /opt/gradlew

Android SDK

You will need to download SDK manually without Android Studio bundled, SDK tools only, check the link here to get the URL.

Like Gradle, we’ll save it to /tmp and get it extracted in /opt

$ wget 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip' -P /tmp \
&& unzip -d /opt/android /tmp/sdk-tools-linux-4333796.zip \

The Android software development kit (SDK) includes different components, including SDK Tools, Build Tools, and Platform Tools. The SDK Tools primarily includes the stock Android emulator, hierarchy viewer, SDK manager, and ProGuard. The Build Tools primarily include aapt (Android packaging tool to create .APK), dx (Android tool that converts .java files to .dex files). Platform Tools include the Android debug shell, sqlite3 and Systrace.

Most important packages are platform-tools, tools and emulator. Run this to install them quickly:

$ yes Y | /opt/android/tools/bin/sdkmanager --install "platform-tools" "system-images;android-*28*;google_apis;x86" "platforms;android-28" "build-tools;28.0.3" "emulator"
  • platform-tools contains adb

  • tools contains avdmanager and sdkamanager

  • emulator : run emulator

  • system-images;android-*28*;google_apis;x86 : use to create avd

Accept all licenses of Android SDK

$ yes Y | /opt/android/tools/bin/sdkmanager --licenses

Seems good, now create an avd test

$ echo "no" | /opt/android/tools/bin/avdmanager --verbose create avd --force --name "test" --device "pixel" --package "system-images;android-28;google_apis;x86" --tag "google_apis" --abi "x86"
  • — name: device’s name.

  • — abi: CPU architecture.

  • — tag google_api: support Google API.

Check one more time to see if it works!

$ /opt/android/emulator/emulator -list-avds 
# Expected Result: test

3. Set up environment variables

Edit your .bashrc or any config files that you’re familiar with:

  • GRADLE_HOME=/opt/gradle/gradle-5.4.1

  • ANDROID_HOME=/opt/android

  • PATH=$PATH:$GRADLE_HOME/bin:/opt/gradlew:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools

  • LD_LIBRARY_PATH= $ANDROID_HOME/emulator/lib64:$ANDROID_HOME/emulator/lib64/qt/lib

Then reflect the changes with source ~/.bashrc

Note: The order of PATH variable is important because android provides 2 executed file for emulator, one is packaged in /android/emulator and another is in /android/tools/bin. We’ll use /android/emulator to run android device.

More detail is this SO answer.

4. Run emulator

Here are some minor steps before running emulator:

  • Stop any running emulators with ADB.

  • Start emulator in the background with flag -no-window and -gpu off

  • Turn off animation to avoid flaky tests.

Some test cases will perform an action on a view that might be not visible on a small screen — so we set the emulator up to have a high resolution (1440x2880)

function wait_emulator_to_be_ready() {
  adb devices | grep emulator | cut -f1 | while read line; do adb -s $line emu kill; done
  emulator -avd test -no-audio -no-boot-anim -no-window -accel on -gpu off -skin 1440x2880 &

boot_completed=false
  while [ "$boot_completed" == false ]; do
    status=$(adb wait-for-device shell getprop sys.boot_completed | tr -d '\r')
    echo "Boot Status: $status"

    if [ "$status" == "1" ]; then
      boot_completed=true
    else
      sleep 1
    fi
  done
}

function disable_animation() {
  adb shell "settings put global window_animation_scale 0.0"
  adb shell "settings put global transition_animation_scale 0.0"
  adb shell "settings put global animator_duration_scale 0.0"
}

wait_emulator_to_be_ready
sleep 1
disable_animation

Start the emulator with sh start.sh or ./start.sh and wait until Boot Status: 1 which means the device is fully loaded and ready to use.

Check again by enter bg to see the background jobs.

$ ./start.sh
emulator: WARNING: Your AVD has been configured with an in-guest renderer, but the system image does not support guest rendering.Falling back to 'swiftshader_indirect' mode.
WARNING: change of renderer detected.
checkValid: hw configs not eq
emulator: Cold boot: different AVD configuration
Your emulator is out of date, please update by launching Android Studio:
 - Start Android Studio
 - Select menu "Tools > Android > SDK Manager"
 - Click "SDK Tools" tab
 - Check "Android Emulator" checkbox
 - Click "OK"

Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 
Boot Status: 1
emulator: INFO: boot completed
emulator: INFO: boot time 13200 ms
emulator: Increasing screen off timeout, logcat buffer size to 2M.
emulator: Revoking microphone permissions for Google App.

Check running emulators

$ adb devices
List of devices attached
emulator-5554   device

We have an Android emulator running in the container successfully!

5. Build docker image

Open a new terminal tab, stop android-container and commit your changes to create a new Docker image:

$ docker stop android-container && docker commit android-container android-container:v1

Test once again:

$ docker images
# Expected result: REPOSITORY          TAG
                   android-container   v1

For further extending with more configurations (ex: Flutter), let’s wrap things up and finalize the Dockerfile

FROM ubuntu

LABEL maintainer "codecaigicungduoc@gmail"

WORKDIR /

SHELL ["/bin/bash", "-c"]

RUN apt update && apt install -y openjdk-8-jdk vim git unzip libglu1 libpulse-dev libasound2 libc6  libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxi6  libxtst6 libnss3 wget

ARG GRADLE_VERSION=5.4.1
ARG ANDROID_API_LEVEL=28
ARG ANDROID_BUILD_TOOLS_LEVEL=28.0.3
ARG EMULATOR_NAME='test'

RUN wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip -P /tmp \
&& unzip -d /opt/gradle /tmp/gradle-${GRADLE_VERSION}-bin.zip \
&& mkdir /opt/gradlew \
&& /opt/gradle/gradle-${GRADLE_VERSION}/bin/gradle wrapper --gradle-version ${GRADLE_VERSION} --distribution-type all -p /opt/gradlew  \
&& /opt/gradle/gradle-${GRADLE_VERSION}/bin/gradle wrapper -p /opt/gradlew

RUN wget 'https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip' -P /tmp \
&& unzip -d /opt/android /tmp/sdk-tools-linux-4333796.zip \
&& yes Y | /opt/android/tools/bin/sdkmanager --install "platform-tools" "system-images;android-${ANDROID_API_LEVEL};google_apis;x86" "platforms;android-${ANDROID_API_LEVEL}" "build-tools;${ANDROID_BUILD_TOOLS_LEVEL}" "emulator" \
&& yes Y | /opt/android/tools/bin/sdkmanager --licenses \
&& echo "no" | /opt/android/tools/bin/avdmanager --verbose create avd --force --name "test" --device "pixel" --package "system-images;android-${ANDROID_API_LEVEL};google_apis;x86" --tag "google_apis" --abi "x86"

ENV GRADLE_HOME=/opt/gradle/gradle-$GRADLE_VERSION \
ANDROID_HOME=/opt/android
ENV PATH "$PATH:$GRADLE_HOME/bin:/opt/gradlew:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools"
ENV LD_LIBRARY_PATH "$ANDROID_HOME/emulator/lib64:$ANDROID_HOME/emulator/lib64/qt/lib"

ADD start.sh /

RUN chmod +x start.sh

6. Build project and run tests

Check out your project configurations and build the ocker image with appropriate arguments:

$ docker build \
--build-arg GRADLE_VERSION=5.4.1 \
--build-arg ANDROID_API_LEVEL=28 \
--build-arg ANDROID_BUILD_TOOLS_LEVEL=28.0.3 \
--build-arg EMULATOR_NAME=test \
-t android-container .

Enter the top level of your project directory and run:

docker run --privileged -it --rm -v $PWD:/data android-container bash -c ". /start.sh && gradlew build -p /data"
  • -it: interactive mode.

  • rm: remove volume after the process is completed.

  • -v $PWD:/data: mount your directory into the container:/data

  • bash -c “ . /start.sh && gradlew build -p /data”: start the emulator and build project.

Voilà! We have the whole testing process run independently on docker container. Let’s get back to other stuff while waiting for the report. 😝 😝

For more detailed about build steps and sample, please check out this GitHub repo. I’ve made a cool sample with Sunflower project.

If you have any questions, feel free to ask, reach out to me via codecaigicungduoc@gmail.com or creating a new issue on my github repo.

Thanks for reading 🙇 🙇 🙇

Posted on by:

Discussion

markdown guide
 

Tried this in my mac air laptop (early 2014) and is stuck at emulator error. Will this setup work in a laptop ?. What other stuff do i need in the container (KVM etc ?) to make it work ?

~$docker run --privileged -it 67d6089da088 bash -c ". /start.sh"
Ubuntu 18.04.4 LTS \n \l

  • daemon not running; starting now at tcp:5037
  • daemon started successfully emulator: ERROR: x86 emulation currently requires hardware acceleration! Please ensure KVM is properly installed and usable. CPU acceleration status: KVM requires a CPU that supports vmx or svm More info on configuring VM acceleration on Linux: developer.android.com/studio/run/e... General information on acceleration: developer.android.com/studio/run/e....
 

You will need to do this under virtualbox. I dont have a mac, but it can be made to work, a colegue has it set up to run I believe.

 

ANDROID_HOME is deprecated, I see an updated script on github, care to walk us through the update? Struggling a little with the emulator when I upgrade SDK and then try use emulator afterwards.