Build Variants are different versions of your app, built from the same project. In this blog post I will show you how to create different build variants for your app, and how to setup your project structure to enable you to write different tests for different variants, while sharing the test code that is applicable to all build variants. And finally I will show you how to run your tests for all build variants, and for specific build variants.
Examples of build variants are ‘debug’ and ‘release’ variants of an app. Usually the differences between debug- and release variants are not on a functional level, but on a technical level. But it’s also possible to create build variants of an app that contain functional differences. An example of this is an app that has a free and a paid variant: the paid variant usually offers features not available for the free variant of the app. It makes sense then to create different tests for the different variants where they differ, and write a single test that you can run for all variants for the functionality that they have in common.
As an example I will be using one of my own apps, and show you how I added the build variants. If you would like to see the source code, you can take a look here:
And finally, before we begin: Why would you want to do this?
The most obvious reason is that your project already has build variants, and you want to be able to test these with specific tests. If you just dump these tests in your androidTest folder, you will end up with test failures for all build variants that haven’t got the features that you’re testing.
My personal reason for adding build variants to my project was the other way around: to make testing easier. One build variant connects to a remote API for data, and the other will use data that’s locally stored. That way I can build (and run) reliable UI tests using local data, and build separate integration tests at the API level for the real stuff.
If you’re curious about using local data for testing, I have written a blog post about it: https://www.testchamber.nl/uncategorized/using-build-variants-to-stub-your-api/. But I would advise you to read this post first if you are not familiar with build variants.
Build Types, Flavors and Dimensions
The powerful thing about build variants is that it allows you to specify (test)code for a specific version of your app, but also to share code between different versions of your app.
This works by specifying different properties in your Gradle.build file: BuildTypes, flavorDimensions, and productFlavors. You can create different source folders for these properties. That means that you can create a folder that contains code that belongs to a certain flavorDimension, or code that’s part of a certain productFlavor.
Your app’s build variants are generated by combining the flavors and build types that you specified, and the resulting app is built using the source folders that belong to these flavors and buildTypes.
Here’s an example using just productFlavors (flavorOne, flavorTwo) and buildTypes (debug, release) and the resulting build variants:
- By default a project has a /src/main folder containing the app code , and a /src/androidTest folder that contains the code for our instrumented tests.
All build variants can always access /main and /androidTest.
- We can create new folders for code, by using productFlavor names. Example: code in /src/flavorOne or /src/androidTestFlavorOne is only accessible by our flavorOneDebug and flavorOneRelease build variants. That’s because they belong to the ‘flavorOne’ productFlavor. This also means that other productFlavors can’t access this code.
Folders with a productFlavor name are only accessible by build variants of that productFlavor.
- Another option is to use buildType names for folders. That means that we can use /src/debug and /src/androidTestDebug and the code in these folders is accessible by all build variants of the debug build type. In our example those are: flavorOneDebug and flavorTwoDebug, so it’s not accessible by our release build variants.
Folders with a buildType name are only accessible by build variants of that BuildType.
- If we want to get really specific, we can also use whole build variant names for our source folders. Code in /src/flavorOneDebug and /src/androidTestFlavorOneDebug is only accessible by that exact specific build variant. So no other build variant can access that code, not even flavorOneRelease.
Folders with a build variant name are only accessible by that exact build variant.
As you can imagine, by using a combination of different source folders, you are able to create (test)code that applies exactly to those build variants that you need it to, without having to create duplicate code.
In the next section I will show you (by example) how to add the build variants to your gradle.build file and how to add the source folders to your project.
Adding Build Variants to your Project
The first step is selecting your gradle.build file. If you are in the ‘Android view’ (select ‘Android’ from the drop down menu at the top), you can open the ‘Gradle Scripts’ section. Now select the build.gradle file for the module that you would like to have build variants. My project has two modules (apiservice and app), but chances are that your project will have only one module (app).
The second step is defining our build variants, see the example below.
First we define the ‘flavorDimensions’. You can pick any name you like, I just went with “version” because I couldn’t think of anything better. Second, we will define our ‘productFlavors’. Again you can pick whichever names you like. My app will have two build variants called ‘localData’ and ‘retrofitdata’, and both have two properties: ‘dimension’ and ‘versionNameSuffix’. The first assigns the productFlavor to a dimension, the second determines a suffix for the name of each version when we build the app.
As you can see my project also has one buildType defined (release). By default the app also has a ‘debug’ buildType, even when it hasn’t been explicitly defined. So now we’ve got buildTypes, flavorDimensions and productFlavors and if you combine these, you will get the different build variants. Without the flavorDimensions and productFlavors, my project would have only buildTypes defined, resulting in the following build variants: debug, and release.
Now that we’ve added the flavorDimensions and productFlavors, we get the following build variants: localdataDebug, localdataRelease, retrofitdataDebug, and retrofitdataRelease.
The build variants created for us are all the possible combinations of our productFlavors and buildTypes:
|flavors for ‘version’ / buildTypes||debug||release|
You may be wondering now where the dimensions fit in, because they don’t seem to affect our build variants. That is because this example uses only one flavorDimension. The way the build variants are combined is as follows: [ each productFlavor for a dimension] + [ each productFlavor for another dimension etc] + [each buildType]. Let’s look at an example to (hopefully) make this a bit more clear:
This example has two flavorDimensions and three productFlavors. This results in the following build variants: [a productFlavor for flavorDimension ‘version’] + [a productFlavor for flavorDimension ‘test’] + [buildType]. The order of the flavorDimensions is determined by the order that you list them in in the build.gradle file. The end result is this:
So all productFlavors that belong to the flavorDimension ‘version’, are combined with each productFlavor that belongs to the flavorDimension ‘test’, and each of the resulting combinations gets combined with every buildType.
After adding the flavorDimensions and productFlavors to a project, Gradle will ask you to re-sync the project (and if it doesn’t, you can do it manually by pressing the elephant icon in the Android Studio tool bar). You can see the build variants in the ‘Build Variants’ section of Android Studio: select ‘View’ from the top menu bar, ‘Tools Windows’ and ‘Build Variants’. You should now see this:
With our current set up all the build variants will produce an app using all the same files and code, because we haven’t created different source folders yet. That’s our next step: creating the folders where we can put the different files and tests for our build variants.
To see which folders Android Studio recognises as the source folders for our different build variants, we can run a Gradle task that will output a list with the appropriate folders. Select ‘View’ from the top menu bar, select ‘Tool Windows’ and select ‘Gradle’. Now select you module (‘app’ for example), Tasks, Android and double click ‘SourceSets’.
You should now see some output in your terminal that looks a little like this:
This output shows you all the different source folders that you can create for specific build variants. As mentioned earlier in this post, the folder structure that you use determines which build variant can access that code. For example, by default the source folder for app code is /main/java. The code that is stored there is accessible by all build variants. By contrast the code in /retrofitdataDebug/java is only accessible by our retrofitdataDebug build variant, while data in retrofitdataRelease/java is only accessible by our retrofitdataRelease variant.
It’s also possible to use source folders that allow all build variants of a certain flavorDimension, or a certain productFlavor to access the code. If we store code in /retrofitdata/java, then both our debug and our release build variant can access the code.
Creating Source Folders
First we are going to create a folder where we can put the instrumented tests for our retrofitdata productFlavor. Instrumented tests for all variants are stored in the androidTest/java folder. In the screenshot of the output of the Gradle task that we ran earlier, we can see that the folder for the retrofitdata productFlavor is apiservice/src/androidTestRetrofitdata/java. My module is called apiservice, so if I ran the task for the app module it would have been app/src/androidTestRetrofitdata/java.
To create the folder, you will have to switch to the ‘Project’ view by select ‘Project’ from the drop down menu at the top. You should be seeing this:
Currently we’ve got a ‘main’ folder (for app code), a ‘test’ folder (for unit tests) and the ‘androidTest’ folder (for instrumented tests). These are accessible by all build variants. We’re going to add our ‘androidTestRetrofitdata’ folder manually by right clicking the ‘src’ folder and selecting ‘New’, and ‘Directory’:
And enter the folder name:
After pressing ok, the result should be as follows:
I have spent about a day getting Android Studio to recognise this as a source set for the retrofitdata instrumented tests, and in the end it would only recognise it, if there was also a retrofitdata/java folder (which is where any app code for the retrofitdata build variant would be placed). Creating this folder works a bit differently. Again, right click the ‘src’ folder, but this time select ‘New’, ‘Folder’ and ‘Java Folder’:
Select the target source set of choice, and then click ‘Finish’:
The result looks like this:
You have now successfully created the source folders for one build variant. For others you can repeat this process.
If you want to add packages to your java folder, make sure that the right build variant (one that has access to those source folders) is selected. You can then add packages, by right clicking the java folder and selecting ‘New’, and ‘Package’.
I’ve added my nl.testchamber.apiservice packages to the androidTestRetrofitdata/java folder and to my retrofitdata/java folder. Now when I switch back from the ‘Product’ view to the ‘Android’ view (in de drop down at the top), and select one of the retrofitdata build variants, it looks like this:
We’ve got our project set up, time to add some tests!
To add tests for a specific flavor or build variant you can select the package, right click and add a new Java class or Kotlin file and start adding your tests. In my example, any code that I want to use, or any tests I want to run for all build variants, will be placed in my androidTest folder. Any test that is specifically for my build variants that have the retrofitdata productFlavor, goes into my androidTestRetrofitdata folder.
You can treat the classes in the different folder as if they are part of the same project, because they are. That means you can instantiate objects and inherit from other classes, just as you normally would. There are a few things to keep in mind though:
- You can’t access classes that are part of source folders that your build variant has no access to. That means that classes in the androidTestRetrofitdata source folders do not have access to classes in androidTestLocaldata folders.
- You cannot use duplicate class names for classes that belong to the same package, even when one is in androidTest and the other is in androidTestRetrofitdata.
- You can use the same class names if the files belong to different source folders that are mutually exclusive. So you can have identical classes in androidTestRetrofitdata and androidTestLocaldata.
If you already have test classes in androidTest, and you would like to move those to one of the other packages, there’s an easy way to do it: right click the class, select ‘Refactor’, and ‘Move’ (or press ‘F6). I have created a file called ‘MoveTest’ as an example:
After selecting the ‘Move’ option, you will get some options:
Using the ‘Leave in same source root’ drop down menu, you can select a different destination. If the right option isn’t displayed (it wasn’t always there for me) select the three dots next to the drop down menu. Also make sure that the right build variant is selected.
Press ‘OK’, ‘Refactor’ and you should be good to go!
Running Tests for Build Variants
You can use Gradle Tasks and the Android Studio Terminal to run the tests for the different build variants. You will find the terminal at the bottom:
To see a list of available Gradle tasks run the following command (remove the dot and slash if you are using windows):
This outputs a list of the available Gradle Tasks, and a few of those allow us to run our tests. To run tests on all connected devices for all build variants enter:
To run the tests for a specific build variant you can use another task, for example:
It is also possible to run tests from a specific class. It’s possible to do it for all variants, or a specific variant. The example is for all variants:
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=nl.testchamber.nl.MoveTest
If you would like to read more about build variants, and other options available, please check out the official documentation: https://developer.android.com/studio/build/build-variants