Have you ever thought how to run UI tests without lots of devices, how to publish the app without visiting the Google Play Console or maybe how to make a “program” that will check your code with lint, then run the unit and the UI tests, and finally, publish the app on Google Play? If so and you did not find a solution, I will show you how it can be done.

As a CI service, we will use CircleCI because they give you 1500 free build minutes per month and also it is possible to build projects that are hosted in the private repositories, but CircleCI only works with GitHub and Bitbucket. For running UI tests we will use Firebase Test Lab because they allow you to run up to 15 instrumentation tests per day. Dropbox to store the signing key and its credentials. Also, we will use Fastlane, because it simplifies some of the processes, like publishing the app on Google Play.

Preparation

Before we can start implementing the workflow, we need to get the keys. What key do we need? The Dropbox key — it will be used for downloading the archive with signing key and its credentials from your Dropbox account. The Firebase key — with this key we will be able to run the instrumentation tests in Firebase Test Lab. The final one is the Google Play key — this key is going to be used for publishing the app on Google Play.

Also, we will write two bash scripts. The first one is for downloading the signing data and the second one is for running the instrumentation tests. Actually, they are not so important and you can do everything without them, but in my opinion, with them, the solution will be cleaner.

Obtaining the keys

How to get the Firebase key?

Go to Google Cloud Platform, Select the Firebase project, Then select the IAM & Admin > Service accounts in the left menu, Click the Create Service Account button, Follow the instruction, Give an Editor role for the service account, Save the key as a JSON file and name this file as gcloud_key.json.

How to get the Google Play key?

Go to Google Play Console, In the left menu select "Settings", Go to API access section, Click the CREATE SERVICE ACCOUNT button, Follow the instruction (it the same as you did to get the Firebase key), Give an Editor role for the service account, Save the key as a JSON file, also name this file as gplay_key.json, Go back to the Google Play's Settings page and click GRAND ACCESS, In the role field select Release Manager.

How to get the Dropbox key?

Go to the My apps section in Developers page, Create an app, Click the Generate button in the OAuth 2 section, Save the key somewhere.

Do not commit these keys into the repository.

Bash scripts

Now we will create two bash scripts that will be located in the project. Create a folder in the root of your project and name the folder as ci. Then, copy-and-paste the given scripts and do not forget to make them executable ( chmod +x filename ).

Name this script as download_ci_data.sh. This script will download the signing data from your Dropbox account.

Name this script as run_firebase_tests.sh. This script will upload the APKs and the instrumentation tests will be run. The --async flag means that the script will not wait until the tests will be finished.

Commit this folder into the repository.

Add variables

Before we can add the variables, we must create a project in CircleCI.

Log in with your Github or Bitbucket account (if you log in with the Github account, you will see the repositories that are hosted in Github, but if you log in with Bitbucket account, you will see the Bitbucket's repositories). After you were logged in, go to ADD PROJECT page and create a project for one of your repositories by clicking the Set Up Project button. On the next page, click the Start building button. For the first time, your project will not be built, but we will fix it later.

After the project was created, go to Settings > Projects and click on the gear button that is located on the right to project’s name. On the next page, go to the Environment Variables section.

Here you need to add 5 variables:

DROPBOX_KEY — copy the Dropbox key, GCLOUD_SERVICE_KEY — copy the text that is inside the gcloud_key.json, GOOGLE_PLAY_KEY — copy the text that is inside the gplay_key.json, GOOGLE_PROJECT_ID — copy the Firebase project id, SIGNING_ARCHIVE_NAME — the name of the archive with the signing key and its credentials.

Signing config

The last but not the least, you should add a singing config to the app's build.gradle. We can sign the app via Fastlane, but unfortunately, the key's credentials will be printed in the log, and from my perspective, it is not safe.

First thing first, create a file in the root of the project and name it signing.properties and add these lines to the file:

keyAlias=debug

keyPassword=debug storeFile=../signing.properties storePassword=debug

Then in the app's build.gradle, read the created file and load its data into the properties.

Commit the properties file. We do this file because signing.properties is required by Gradle, so without it, the project will not be synced. If you want to sign the app, you will need just to replace this signinig.properties file with the correct one.

Also, generate a signing key and fill the signing.properties file with proper credentials. Then create an archive with the key and the signing.properties file, additionally, name the archive with the name that you have entered in the SIGNING_ARCHIVE_NAME variable. After that, upload the archive into the root of your Dropbox account.

Fastlane

We need to initialize Fastlane so it will know app's package, where the key to access Google Play is. To do this step, you will need installed Fastlane on your machine (how to install Fastlane).

Firstly, open a terminal and go to the project's folder, then execute this command fastlane init .

Then, enter app's package name, after that, you will be asked to enter the path to the JSON file, enter gplay_key.json.

After that, the wizard asks whether you need to download metadata from Google Play or not — press n, because we do not need this data.

After an initialization, you will have a fastlane folder and a Gemfile in the root of the project. Also, inside the fastlane folder, there will be two files inside it: Appfile and Fastfile. Commit all these files into the repository.

Now we are ready to implement the lanes. There will be 5 lanes that are:

Running the unit tests, Assembling the debug APK, Assembling the android test APK, Running the instrumentation tests, Assembling the release ABB and publishing it on the internal track.

These lanes must be written in the Fastfile.

The first 3 lanes just run the Gradle tasks.

The instrimentation_tests task runs the lanes that will assemble needed APKs and after that, runs the run_firebase_tests.sh.

The last lane runs a Gradle task that will assemble a release ABB and uploads it to Google Play. The lane_context[SharedValues::GRADLE_ABB_OUTPUT_PATH].to_s variable contains path to the ABB. If your app's path is different from the default one, you will need to enter the right path.

CI config

Before we continue with the CI's config file, it would be nice if you could read this page.

This is a config file for CircleCI. At first, read it through and then I will describe each step in details.

We will use the circleci/android:api-28-alpha docker image because it has everything that we need for our building process, like, Java SDK, Android SDK, gcloud, Ruby and so on.

Also, I add these lines, because I do not feel like writing them in every job.

defaults: &defaults

docker:

- image: circleci/android:api-28-alpha

environment:

JVM_OPTS: -Xmx3200m

You can add these lines in every job by adding that line <<: *default . If you want to learn more about it, search for the anchor in this article.

Run the unit tests

Let's have a look at the unit-tests job.

Firstly, the job checkouts the code from the repository, then restores the cache (we will cache the .gradle folder to make builds faster), after that, the job installs Fastlane and its dependencies.

After the installation, the job is ready to run the unit tests, therefore, it runs the unit_tests lane by executing bundle exec fastlane unit_tests .

After running the unit tests, the job saves the reports that are located at app/build/reports in artifacts. You can get these files in the build's page in the Artifacts section.

Finally, the job saves the .gradle folder in the cache.

Run the instrumentation tests

The instrumentation-tests job is similar to the unit-tests job.

The job checkouts code, restores cache, installs Fastlane.

After all these steps, the GCLOUD_SERVICE_KEY variable is going to be stored in a file, so after that, it will be possible to authorize with this key and run the instrumentation tests in Firebase Test Lab.

To run the instrumentation test, the job executes bundle exec fastlane instrumentation_tests .

I want to emphasize that the job will not wait until all of the tests will be finished. This step finishes itself right after the APKs were uploaded. I decided to do this because I do not want to waste build time on waiting for the results. In my opinion, you can check the result in the Firebase console and continue the workflow. But if you do not think so, you can remove the --async in the run_firebase_tests.sh script.

Publish the app

As you can see, the job is quite the same as the previous ones and that is why we will skip some steps.

Firstly, the job stores the value of the GOOGLE_PLAY_KEY variable into a file. This file will be used by Fastlane to publish the app (do you remember that we set key’s path when we were initializing Fastlane?).

Then, the archive with signing data is going to be downloaded. Also, in the next step, Fastlane downloads project's metadata.

After that, the job executes bundle exec fastlane deploy_internal and the release version of the app will be assembled and publish in the internal track on Google Play.

Workflow

The workflow will look like this. We run the unit and the instrumentation tests in parallel. After that, there will be a hold step, it means that the build process is stopped and it is waiting for your approval. As I said early, I added this step because I do not want to waste building time, so you check the instrumentation tests results and if everything is OK, you approve the step. However, you can remove the --async flag and the hold step too, but it will increase instrumentation-tests step's build time.

Here is the workflow’s code. Its name is deploy-internal and it contains 4 jobs. The unit-tests and instrumentation-tests jobs are running in parallel. The hold job waits for the end of the unit-tests and the instrumentation-tests and also it requires user's approval. The final job is deploy-internal, it will not be run until the hold job was approved.

Summary

Let's sum everything up. We made a quite simple solution that runs the tests and published the app in the internal track on Google Play and it is completely free, even for the private projects.

The workflow is extremely simple and it should be improved, but how? I would make one more workflow that would build the app and run the Unit tests for every commit, while the deploy-internal workflow would be run only for branches that matched some pattern (e.g. internal/* ). Also, I would change the run_firebase_tests.sh script, so it would get some arguments, for example, APK paths, device ids, OS versions. Thus, I would have a much more flexible solution.

Here is a GitHub repository for this article.

If you have any questions, feel free to ask me in comments or on LinkedIn and Twitter.