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.
I will get straight to the point: it is not an application. What does this mean for you? You won’t be able to:
- Specify the <application> tag in the android manifest.
- Launch it as an application on a device.
Why is this an issue?
- 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.
- 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!
In the above example you get a result of a Gradle task run. For this you have to do the following:
- Create an instance of GradleRunner.
- 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.
- 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.
- 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.
- 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?
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:
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:
So what you do here is that you:
- Create a new Properties object.
- 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.
- Open the file and load it’s content into the properties object.
- 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.
With the above code you can get the tasks that have run and check if a given one is present or not.
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:
- Adding dividers between the test case outputs.
- Customizing the test case console outputs text appearance.
- 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:
- Get a reference to a Gradle Logger instance in your test class.
- Do the logging in the method that is annotated with @Before, so it will be executed before each test case runs.
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:
And you can find the underlying logic here:
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.
Looks cool, but how does it do the magic? I will explain the five steps below.
- Copy the chars that are needed to be written on the console to a new array.
- Add the color to the given text,
- Create a coloring code based on the ANSI escape codes.
- Add the coloring code before the given text.
- Add the escape char before the coloring code.
- Add the needed indentation to the given chars.
- Add the ending for the coloring escape sequence.
- 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:
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.
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.