Write your tests for your Android libraries and plugins

During your Android developer career, you may reach a point where you will have to develop Android libraries or Gradle plugins. In this article, we will discuss the most important aspects and differences of testing these.

During your Android developer career you may reach a point where you will have to develop Android libraries or Gradle plugins. In this article I will write about the differences of how you should test them. Of course most of the testing is similar to the testing of Android apps, but there are some things you should be aware of. 

Testing libraries

I will get straight to the point: it is not an application. What does this mean for you? You won’t be able to:

  1. Specify the <application> tag in the android manifest.
  2. Launch it as an application on a device.

Why is this an issue?

  1. This means that you can’t set some specific things for your tests. The one I faced in the past was that I wasn’t able to set android:usesCleartextTraffic to true. This meant that test cases, in which network calls were made to non-secure endpoints were failing starting from API level 28 (more on this limitation here). Of course as we learned in the previous articles, we should limit firing up real network calls and try to mock them where possible. In this case, a mock server is not enough, you have to mock things on the client side as well.
  2. This limits the options for UI tests, for example you cannot write UIAutomator tests. Of course, in most cases this is not an issue, but for some it can be. Just imagine creating a library that provides different charts (views) for the app that uses it.

How to mitigate?

Well, trivial problems like not having an application requires trivial solutions: create a test application, which will use your library as a dependency. I think this is the easiest and smoothest way to deal with this problem. It does not have to be too complicated, but this really depends on your needs. A best approach is when your test app is able to showcase different capabilities of your library.

Depending on the product you are developing, you might have your library in a multi-module project, which has an application already, so in that case you do not have to create a new test application, just use the existing app.

Testing Gradle plugins

When you are developing Gradle plugins, you can write them in different JVM languages, such as Java, Groovy or Kotlin. Luckily, thanks to this, you can use the good old JUnit framework to test most of the things.

You might need functional tests as well. Plugins usually add new tasks to the given project or modify the output of an existing task, so you should be able to test Gradle tasks. This can be done with the Gradle Testkit. With the Testkit you can run Gradle tasks in tests, and analyze the build results. This sounds great, let's see how to do it!

final BuildResult buildResult =
       GradleRunner.create()
                   .withProjectDir(getTestDir())
                   .withArguments(“taskName”)
                   .forwardOutput()
                   .build(); 
Copy code

In the above example you get a result of a Gradle task run. For this you have to do the following:

  1. Create an instance of GradleRunner.
  2. Set the directory (File) for the project, which will run the task. I use a method here to get the proper directory, which I will explain later.
  3. Add the arguments of the Gradle command. You should add the name of the task you want to run, but additional options can be also configured.
  4. Forwarding the output is optional, but it can help with debugging, as you will see the output of your test on the console, as you have run the task.
  5. Call the build method to get the build result. I will explain what you can do with it.

I will explain things in order, see them below. 

What does my getTestDir() method do?

@Rule
public TestName testName = new TestName();

@NonNull
private File getTestDir() {
   return new File(
          testProjectDir.getAbsolutePath() + "/" + testName.getMethodName() + "/");
}

Copy code


So as I mentioned, it returns the project directory, which will run the given Gradle task. As you can have multiple test cases, I think it is a good idea to have different working directories for them, so you can be sure that the order of the execution does not affect the results. I just concat a base path for testing, and each test case will have a directory named as the test. For getting the name of the actual test just simply create and use a @Rule

Depending on the use case, you can decide either to create a temporary directory to run the test cases, or to use a non temporary directory, so you will be able to check its content later. Can be useful if you want to debug things. If you want to create a temporary folder, just use a rule for it:

@Rule
public TemporaryFolder tempFolder = new TemporaryFolder(); 
Copy code

If you use a non-temporary folder for your test cases, the best way to make sure the results of the task runs are not affected by the previous runs, delete the test directory. This can be easily done if you create a method and annotate it depending on the solution with @BeforeClass or @Before.

How to add content to your test folders?

Creating an empty directory is not enough: you have to set it up with the required contents. You have to put a build.gradle file there at least, but of course you can add settings.gradle, gradle.properties, local.properties etc. You can add the build.gradle in two ways. One way is to create the content as a String and write it to a file. Could work, but it would be really cumbersome to create more complex content, let alone reading them. I would prefer creating actual test resource files and putting them in the resources folder under the given source set:

The benefits of this approach is that you will have the same features as you would have in your applications build.gradle, like code completion, Gradle sync, etc. You just have to make sure to copy it to the proper folder before running your tests.

Pro tip from the experienced: add a few lines of comments somewhere in your test file or in the test build.gradle itself and explain what this Gradle file used for.

Having multiple test cases might require a lot of different build.gradle files. To avoid having a lot of duplicated content, try to put common parts in gradle files that will be used by multiple smaller gradle files.

apply from: "my-common.gradle"

You just have to add to the above line the given Gradle file and it will contain all the contents from the Gradle build file named “my-common.gradle”. Please note that you can play with the path, so it can be in a subdirectory as well.

Including properties from different gradle.properties file is a bit more complicated, but not impossible:

final Properties myProperties = new Properties()
final File myPropertiesFile = new File(getProject().getProjectDir().getAbsolutePath() + ("/myproperties.properties"))
myPropertiesFile.withInputStream {
   myProperties.load(it)
}
def someProperty = myProperties.get("version") 
Copy code


So what you do here is that you:

  1. Create a new Properties object.
  2. Get a File reference to the Properties File you from where you want to get the properties. In my example, it's in the root project.
  3. Open the file and load it’s content into the properties object.
  4. Now you can access those properties as well.

What can you do with the BuildResult?

As the name suggests, one thing you can get is to get the build result it provided. In some cases this is what you want, just call the getOutput() method.

The more advanced thing you can do, is to access the tasks that have run, check their order, result, etc.

final List buildTasks = buildResult.getTasks();

final Optional processDebugManifestTaskOptional =
       buildTasks.stream()
                 .filter(buildTask -> buildTask.getPath().equals(":processDebugManifest"))
                 .findFirst();


assertThat(processDebugManifestTaskOptional.isPresent(), is(true));
Copy code


With the above code you can get the tasks that have run and check if a given one is present or not.

final BuildTask processDebugManifestTask = processDebugManifestTaskOptional.get();

final int manifestProcessIndex = buildTasks.indexOf(processDebugManifestTask);
assertEquals(TaskOutcome.SUCCESS, processDebugManifestTask.getOutcome()); 
Copy code


And as I mentioned, you can check and assert their result, or check their index in the running order, so you can make sure about their order as well. There are a ton of opportunities, if you have something similar, please let me know what you are doing with it.

How to handle test console outputs?

As you add more and more test cases and forward the output to your console you will be experiencing quite confusing and long outputs. There are some techniques on how can you mitigate this. I will introduce three of them:

  1. Adding dividers between the test case outputs.
  2. Customizing the test case console outputs text appearance.
  3. Writing the output to a file.

Adding dividers between the test case outputs is probably the easiest one. We do not have to do anything specific, but add a few log lines between test cases. Here is how to do it:

  1. Get a reference to a Gradle Logger instance in your test class.
logger = Logging.getLogger(this.getClass().getName()); 
Copy code
  1. Do the logging in the method that is annotated with @Before, so it will be executed before each test case runs.
@Before
public void setUpTest() {
  logTestNameInAsciiBox(testName);
Copy code

As you can see, I pass the name of the test to print that out, so it is easy to follow which test is being executed. Also, if you are not satisfied with a simple line of log, and want to make it more readable, consider making it multiline, or put it in an ASCII box, something like this:

There are plenty of 3rd-party libraries out there, feel free to choose the one that you like the most, or see my example below:

So the above box is 5 lines of 3 different types of strings: starting/closing line (top and bottom border), empty line and line with the test name. Code something like this would do the trick:

/**
* Prints multiple lines with the given text in a pretty ASCII text box.
*
* @param text the text to print.
*/
public void logInAsciiBox(@NonNull final String text) {
   final int textLength = text.length();
   final int paddingLength = 30;
   final int lineLength = paddingLength + textLength;

   final StringBuilder stringBuilder = new StringBuilder();

   stringBuilder.append("\n");
   stringBuilder.append(getTestNameBorderLogLine(lineLength));
   stringBuilder.append(getTestNameEmptyLogLine(lineLength));
   stringBuilder.append(getTestNameLogLine(text, paddingLength));
   stringBuilder.append(getTestNameEmptyLogLine(lineLength));
   stringBuilder.append(getTestNameBorderLogLine(lineLength));

   logger.lifecycle(stringBuilder.toString());
}
 
Copy code

And you can find the underlying logic here:

/**
* Prints the top or bottom line. Contains separators only.
*
* @param lineLength the length of the lines.
* @return the line.
*/
@NonNull
private String getTestNameBorderLogLine(final int lineLength) {
   final StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("+");
   stringBuilder.append(concatString("=", lineLength));
   stringBuilder.append("+");
   stringBuilder.append("\n");
   return stringBuilder.toString();
}

/**
* Prints a line with empty content.
*
* @param lineLength the length of the lines.
* @return the line.
*/
@NonNull
private String getTestNameEmptyLogLine(final int lineLength) {
   final StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("|");
   stringBuilder.append(concatString(" ", lineLength));
   stringBuilder.append("|");
   stringBuilder.append("\n");
   return stringBuilder.toString();
}

/**
* Prints the line with the name of the test.
*
* @param testName      the given text to display.
* @param paddingLength the length of padding for the line, excluding starting and ending chars. Padding length
*                      should be an even number, otherwise it will result in uneven line lengths.
* @return the line.
*/
@NonNull
private String getTestNameLogLine(final String testName, final int paddingLength) {
   final StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append("|");
   stringBuilder.append(concatString(" ", paddingLength / 2));
   stringBuilder.append(testName);
   stringBuilder.append(concatString(" ", paddingLength / 2));
   stringBuilder.append("|");
   stringBuilder.append("\n");
   return stringBuilder.toString();
}

/**
* Concatenates the given String the given amount of times.
*
* @param string the String to concat.
* @param times  the number of times it should be concatenated.
* @return the concatenated String.
*/
@NonNull
private String concatString(@NonNull final String string, final int times) {
   return IntStream.range(0, times).mapToObj(i -> string).collect(Collectors.joining(""));
}
 
Copy code

Customizing the test case console outputs text appearance is a bit more tricky, but not impossible.You will have to create your own Writer implementation, and use that with forwardStdOutput(Writer) and forwardStdError(Writer) methods when creating the runner instead of forwardOutput() (see at the beginning of this article). This Writer implementation will demonstrate how to add indentation to each line and choose a different color for the output.

First of all, to change the color I simply used ANSI escape codes, this will change it both on your local environment and on the CI too. For the sake of simplicity, I extended the PrintWriter class and have overridden the write(char[], int, int) method.

@Override
public void write(@NonNull final char[] chars, final int i, final int i1) {
  final char[] charsToWrite = Arrays.copyOfRange(chars, i, i + i1);
  final char[] coloredChars = color(charsToWrite);
  final char[] indentedAndColoredChars = addIndent(coloredChars);
  final char[] toPrint = concatCharArrays(indentedAndColoredChars, coloringEnd);
  super.write(toPrint, 0, toPrint.length);
}
 
Copy code


Looks cool, but how does it do the magic? I will explain the five steps below.

  1. Copy the chars that are needed to be written on the console to a new array.
  2. Add the color to the given text,
@NonNull
private char[] color(@NonNull final char[] chars) {
   final char[] escaped = concatEscapeChar(coloringCode);
   return concatCharArrays(escaped, chars);
}
 
Copy code

- Create a coloring code based on the ANSI escape codes.

@NonNull
private static char[] createColorCode(final int red, final int green, final int blue) {
   return String.format("[38;2;%s;%s;%sm", 
                        red, green, blue).toCharArray();
}

Copy code

- Add the coloring code before the given text.

- Add the escape char before the coloring code.

private static final char[] escapeChar = new char[]{27}; 
Copy code
  1. Add the needed indentation to the given chars.
@NonNull
private char[] addIndent(@NonNull final char[] chars) {
   return concatCharArrays(indentation, chars);
}
 
Copy code

  1. Add the ending for the coloring escape sequence.
private final char[] coloringEnd = 
concatEscapeChar("[39;49m".toCharArray()); 
Copy code
  1. Call super with the new content, lay back in your seat and enjoy the result.

Do not worry, we are nearly there, for the curious I leave the code of the helper methods here:

/**
* Inserts to the start the escape char.
*
* @param chars the given chars to escape.
* @return the given chars with the escape char.
*/
@NonNull
private static char[] concatEscapeChar(@NonNull final char[] chars) {
   return concatCharArrays(escapeChar, chars);
}

/**
* Concatenates two arrays of chars.
*
* @param first  the first array.
* @param second the second array.
* @return the concatenated result.
*/
@NonNull
private static char[] concatCharArrays(@NonNull final char[] first, @NonNull final char[] second) {
   final StringBuilder stringBuilder = new StringBuilder();
   stringBuilder.append(first);
   stringBuilder.append(second);
   return stringBuilder.toString().toCharArray();
}
 
Copy code

Please note, that if you do not want indentation, you can just set the coloring code, before any char is written (for example in the constructor), and close the escape sequence when you printed out everything you wanted (for example you can override the close method for this). This way you would have to set the color once, and reset it once.

And one more thing, writing the output to a file.


This will be a quick one: use a FileWriter for forwardStdOutput(Writer) and forwardStdError(Writer) methods. There are plenty of examples out there how to create one if you need an example, so please use your favourite search engine.

Closing thoughts

I hope you liked this article. I am pretty sure there are other bits and pieces which I could have mentioned about testing libraries and plugins, if you think something vital is missing, do not hesitate to contact me and tell me your thoughts. Also, please stay tuned for the next one, where I will write about how to setup testing in the CI.

No items found.

Explore more topics

App development

Best practices from engineers on how to use Bitrise to build better apps, faster.

Community

Meet other Bitrise engineers, technology experts, power users, partners and join our BUGs.

Company

All the updates about Bitrise events, sponsorships, employees, and more.

Insights

Mobile development, latest tech, industry insights, and interviews with experts.

Mobile DevOps

Learn why mobile development is unique and requires a set of unique practices.

Releases

Stay tuned for the last updates, new features, and product improvements.

Get the latest from Bitrise

Join other Mobile DevOps engineers who receive regular emails from Bitrise, filled with tips, news, and best practices.