Android Jacoco UT coverage

Automation Testing Nov 15, 2021

Unit testing is the key component in any SDLC life cycle which helps in performing the following improvements.

  • Perform sanitary code analysis in order to validate the logic
  • Derive code in the segment to fulfill a certain chunk of requirements
  • Revise the authenticity all the time when any new codebase added to current eco-system

Objectives :

The purpose behind this blog is to highlight the usage of JACOCO for unit testing in Android application development. We will be covering significance of JACOCO tool as well as some of challenging scenarios faced by Halodoc Developers while covering UTs.

Unit testing:

  • Unit testing is process of writing test cases which are having set of instructions to verify the output of current implementation.
  • In the modern paradigm we use many of tools among which JUnit and Mockito are famous and handy to integrate.
  • Mainly JUnit provides utility functions to validate the result and Mockito provides necessary instances on demand.  

To get better understanding over UT concept, please follow Angular Unit Testing blog and iOS code coverage blog. And if you are beginner then it is strongly recommended to complete this codelab to get handy with unit testing.

Now question comes, in a large setup like Halodoc how we determine which code base need to be covered with UT.

JACOCO : what and why?

Jacoco is an open source tool which helps to perform code check and determine the missing UT coverages in production code. By performing code check using this tool, we get a detailed percentages for missed branches (or missed conditions for which test cases had to be covered.)

Here at Halodoc, we have achieved 98% of code coverage through Unit testing, in order to maintain efficiency of code and substitute the unnoticed production issues.

We can prefer JACOCO over other tools for the following reasons

  1. Ease and low cost of integration : Please find detailed steps to integrate in this Article.
  2. Compatibility over multiple platform : JACOCO supports Gradle, Sonar and Maven so it can be easily integrated with remote servers.
  3. Better UX for developers : After running the validation we can get coverage report in HTML, CSV or XML format where we can determine missed, partially missed or covered branches (represented in a colour-coded form as red, yellow or green diamond respectively).
  4. Exclude critical test cases: We can exclude some of files or specific code base. We can mention files to be excluded inside jacoco.gradle.kts -> fileTree.exclude() function. Or to substitute check for specific method we can mark that with annotation @GeneratedAliasExcludeFromTestCoverage .

To get start with JACOCO please refer this documentation.

JACOCO : Where and How ?

Now the question comes that mainly in which type of components we try to achieve code coverage and what is the process to get report. In following components we prefer to cover UT.

Components to be covered for UT

Now let's look at the process how we can generate a Jacoco test report.

Step 1 :  Run the following command in command prompt : ./gradlew jacocoTestReport

Step 2: Find the Jacoco test report link on successful report generation.

Test Coverage Report link

Step 3: Open the report and look for the column named "Missed branches" for missed branches coverage percentages. Our target is to make them 100% if they are applicable.

Test Coverage Report

Step 4: Open the class where we need to achieve 100% branch covered and check for red or yellow diamonds that branch should be covered and converted to green diamond.

Missed branches in the codebase at line number 18

Once the test cases are covered for this unhandled branch repeat from step 1.

We can customise a Jenkins job to access this report in the build information. In Halodoc we are generating reports for continuous as well as release build so we can find it inside StatusTest Coverage Report.

UT Coverage Integration :

There are multiple complex cases where we need to perform some additional hacks to cover branches. We will now look into them in greater detail.

1. MVVM API Integration:

To test these components, the first thing we need to do is to create an instance of ViewModel to pass the mock instance as a parameter in the View Model constructor and create the ViewModel instance.

Then to avoid some complex method call from mock instances we can generate response object manually and return for that particular instruction. This can be achieved by whenever and thenReturn keywords.

In the above example let’s consider demoLiveData.fetchData() returns API data with return type Object. To skip this API call we can manually return our Object.

2. MVP API Integration:

To cover UT in MVP API integration we generally use Call instance from Retrofit. To test that kind of API response we need to predefine the required values in form of mock objects or by using argumentCaptor. To demonstrate this let’s look at the following example.

In above example need to cover both success and error cases as follows. We will begin with initializing necessary instances.

Here we will define all mock and argumentCaptor which are required to perform the given API call above.

Success response

Here we will develop a scenario where we can obtain objects according to our UT requirement using whenever and thenReturn operators as mentioned in 1st case

by setting custom apiCallObjMock(at line no 3) and dummy success Response Data(at line no 11) we will be successfully able to determine the UT for success API response case.

Error Response

Unlike in the success case, in the error case also we need to set apiCallObjMock(line no 8) and error response data. (line no 4,5) and we will be able to test UT for error scenarios in 2 ways by

  1. checking onError is getting called
  2. or by checking onSuccess never gets called (to check that we can pass never() as an argument in verify)

3. API response using Either:

There are some places where we can find Either operator to fetch API response in form of success or failure. To understand it better please refer to this documentation.

This scenario can be more clear by the following example.

In the above example, we are exposing response in form of Either that means it can be failure or success response respective to the left or right value.

Now to create a test scenario for success and failure cases we need to manually pass left or right either value as follows.

As per previous explanations, we will first define necessary objects.

Success scenario

In this case, we are passing the custom response with the successful API response. This response will be obtained as a right element of Either so we will send it as a parameter of right.

And after setting the necessary response we will be able to call an API and it will return a success response and the success UT scenario will be covered.

Failure scenario

In this case, we are passing the custom response within the error response. This response will be obtained as a left element of Either so we will send it as a parameter of right.

And after setting the necessary response we will be able to call an API and it will return a success response and the success UT scenario will be covered.

4. Helper & Static Functions :

In many cases we came across utility functions for which we need to cover test cases. So to perform that we can match the return values and full fill the expectation.

In this example we will generate test data and test the expected value using Assert.assertTrue() or other utility functions by assert.

Similarly for static methods we can directly check output using Assert.

5. Coroutines :

To integrate async operations we usually bind API calls in co-routines scope. To test this kind of async call we need a separate thread to test. That can be achieved by runBlocking block as mentioned below.

6. Hilt (Optional) :

Since we pass arguments in the constructor after mocking that, integrating an additional mechanism for hilt is not mandatory. But still, for information, we can refer to this documentation.

7. String modifications :

Sometimes we find difficulty to cover UT for string literal modifications. Such as while using String.toLowerCase() method we always miss one branch to cover.
In this case segregate the function with other method which will avoid the UT check for dedicated methods.

CI/CD  

In the recent project development eco system we generally prefer automated work to be taken place over manual where it's feasible.

So in reference of that in Halodoc we prefer to follow certain requirement to be fulfilled in order to generate build.

So while developing the QA or Release build we had put a step to comply Jacoco success rate that if it meets 98% then only build generation proceed further otherwise it gets failed to build. So all developers need to full fill the requirement of necessary UT coverage.

Summary

As outlined in this blog, open-source tools like  Jacoco really aids in the SDLC process and simplifies the unit testing activities. The efforts invested in improving our unit testing directly impacts our code quality and have given us ability to catch bugs/errors in the development phase itself than in production.

Join us

Scalability, reliability and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and  if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.


About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek and many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.