Kotlin… Developers are clamoring for it. Some are calling it the future of Android Development. Others are claiming it lacks the tooling to make it “production” ready. At Wayfair, we decided to give Kotlin a try and come to our own conclusions.

Initially, we used it for our “debug” features. These tools were created for developers and employees to help us find and fix issues that customers might see in the wild. But, once Kotlin got it’s foot in the door, it was hard to keep developers from getting excited and hoping to use it everywhere.

We decided to treat Kotlin like anything else going into our codebase; code reviews, testing requirements, and established best practices all had to be met. After a few weeks of adding more and more Kotlin, we started noticing our JaCoCo test coverage dropping. We looked at the code. We looked at the test classes. Everything seemed in order.

this.getApplicationContext()

Before we dive in, a little context about our App. The Wayfair App started out like most e-commerce offerings do. It has a home screen, lists of products, product details, and a checkout process. From those humble beginnings we have been adding more and more features over the last two years and recently started delivering these in weekly releases to our customers.

In order to achieve this pace we decided to re-approach our architecture. What we arrived at is a hybrid of VIPER and Clean, which warrants its own article to give a greater amount of context. In short, we decided to use this combination because we wanted a clear separation of responsibilities between “layers” of our App. This would enable us to increase the code coverage of our unit tests, which prevents developers from creating more work for our QA team, or worse, shipping a bug.

Within our architecture we have the concept of the Repository layer. It’s responsibility includes retrieving our data models from any source, be it API, Room, SQLite, and so on. To illustrate the issues we started seeing in our JaCoCo reports, I will use a simple Repository class for a card management framework that lets us build and arrange views dynamically with information from the server. We call this framework Tarot, and it is actively being used on our “homepage”, enabling us to deliver personalized and timely content to our users.

The investigation begins

Below is a test class in our codebase that exercises the Repository object we use in Tarot. In every run of JaCoCo, we will reuse these tests and will only be modifying the code it is exercising, not the tests themselves.

class TarotRepositoryTest {

   private lateinit var tarotRepository: TarotRepository

   val retrofitConfig: RetrofitConfig = RetrofitConfig()

   @Before
   fun setUp() {
       retrofitConfig.setTarotRequests(mock())
       retrofitConfig.setUnCategorizedNetworkService(mock())
       retrofitConfig.setBasketRequests(mock())
       tarotRepository = TarotRepository(retrofitConfig)
   }

   @Test
   fun getTarot() {
       tarotRepository.getTarot()
       verify(retrofitConfig.tarotRequst).fetchTarotCards()
   }

   @Test
   fun getShippingPromoFetchForEvents() {
       tarotRepository.getShippingPromoFetchForEvents()
       verify(retrofitConfig.unCategorizedNetworkService).shipPromoDataEvents()
   }

   @Test
   fun getShippingPromoFetch() {
       tarotRepository.getShippingPromoFetch()
       verify(retrofitConfig.basketRequests).shipPromoData()
   }

   @Test
   fun getBasketItems() {
       tarotRepository.getBasketItems()
       verify(retrofitConfig.basketRequests).checkoutBasketShow(nullable(CheckoutBasketShow::class.java))
   }

}

Here (below) was the first version of our Tarot Repository in Kotlin. Looks great, doesn’t it? Simple and clear. We get a RetroFit Configuration object from our Dagger Graph and we are able to query our API for the Tarot data to show to the user. As you can see, we seem to be testing everything written below with the tests written above.

@PerFragment
class TarotRepository @Inject constructor(var retrofitConfig: RetrofitConfig?) : TarotContract.Repository {
   override fun setInteractor(interactor: TarotContract.Interactor?) {
       TODO("Code omitted for brevity")
   }

   override fun getTarot(): Observable<Response<MutableList<TarotCard>>>? {
       return retrofitConfig?.tarotRequst?.fetchTarotCards()
   }

   override fun getShippingPromoFetchForEvents(): Observable<Response<WFEyebrowBanner>>? {
       return retrofitConfig?.unCategorizedNetworkService?.shipPromoDataEvents()
   }

   override fun getShippingPromoFetch(): Observable<Response<WFFreeShippingPromo>>? {
       return retrofitConfig?.basketRequests?.shipPromoData()
   }

   override fun getBasketItems(): Observable<Response<WFBasketShipmentsView>>? {
       return retrofitConfig?.basketRequests?.checkoutBasketShow(CheckoutBasketShow())
   }
}

The first result we get tells us we have two untested methods. This seems strange because we don’t have these methods in our code. In fact, they are a product of Kotlin trying to help us, generating these methods silently in the background. So when JaCoCo tests the bytecode, it knows we didn’t test these methods. We can argue that JaCoCo shouldn’t be taking this into account. They are after all just simple getters and setters, but let’s look at this from another point of view.

Let’s re-evaluate our original code and ask ourselves whether we can craft it in a different way. Do we really need this retrofitConfig to be public? Not particularly, so let’s make it private. Also, do we need to modify it? Again, no. Changing retrofitConfig to a val should have no effect on our code, so let’s try that too.

@PerFragment
class TarotRepository @Inject constructor(private val retrofitConfig: RetrofitConfig?) : TarotContract.Repository {
   override fun setInteractor(interactor: TarotContract.Interactor?) {
       TODO("Code omitted for brevity")
   }

   override fun getTarot(): Observable<Response<MutableList<TarotCard>>>? {
       return retrofitConfig?.tarotRequst?.fetchTarotCards()
   }

   override fun getShippingPromoFetchForEvents(): Observable<Response<WFEyebrowBanner>>? {
       return retrofitConfig?.unCategorizedNetworkService?.shipPromoDataEvents()
   }

   override fun getShippingPromoFetch(): Observable<Response<WFFreeShippingPromo>>? {
       return retrofitConfig?.basketRequests?.shipPromoData()
   }

   override fun getBasketItems(): Observable<Response<WFBasketShipmentsView>>? {
       return retrofitConfig?.basketRequests?.checkoutBasketShow(CheckoutBasketShow())
   }
}

After running our JaCoCo task again, we can see that we no longer have any ‘ghost’ methods. One issue down, up next those missing branches.

One great thing about Kotlin is the safe call for nullables – it really does help reduce null pointer exceptions and reduce the number of nested `if` blocks that visually litter a Java class.

Here it is causing us to miss “branches” in our code. What branches? When Kotlin compiles into bytecode, it creates those “nested ifs” for us in order to safely access the property of the nullable object. So, in JaCoCo’s eyes we are missing a branch for every `?.` we have in our code!

This right here seemed to be a hard wall for our adoption of Kotlin. If we can’t use branches as a meaningful measurement of our unit test code coverage, then we could leave large swaths of our code exposed and not know about it. Is there anything we can do?

What if we make retrofitConfig not nullable?

@PerFragment
class TarotRepository @Inject constructor(private val retrofitConfig: RetrofitConfig) : TarotContract.Repository {
   override fun setInteractor(interactor: TarotContract.Interactor?) {
       TODO("Code omitted for brevity")
   }

   override fun getTarot(): Observable<Response<MutableList<TarotCard>>>? {
       return retrofitConfig.tarotRequst.fetchTarotCards()
   }

   override fun getShippingPromoFetchForEvents(): Observable<Response<WFEyebrowBanner>>? {
       return retrofitConfig.unCategorizedNetworkService.shipPromoDataEvents()
   }

   override fun getShippingPromoFetch(): Observable<Response<WFFreeShippingPromo>>? {
       return retrofitConfig.basketRequests.shipPromoData()
   }

   override fun getBasketItems(): Observable<Response<WFBasketShipmentsView>>? {
       return retrofitConfig.basketRequests.checkoutBasketShow(CheckoutBasketShow())
   }
}

As you can see, there are no longer any missing branches!

Conclusion

Now, this approach of modifying the code to match the test is not an exercise we want to continue forever. We have found instances where Kotlin compiles to interesting bytecode under the hood and the hoops you’d have to jump through to get your code coverage up is a larger task than what it’s worth. So, JaCoCo and Kotlin do need to play nicer together in the long term. Meanwhile, we are enjoying the dive into Kotlin and JaCoCo is driving a deeper understanding of the language and its compiler’s choices.

The bottom line is if you your company’s process is rigid and inflexible, then introducing a language that is still maturing might not be the best idea. However, if your team is willing to take the risk and jump through a hoop or two, you really can use Kotlin at scale in production for an Android application and still adhere to your existing development process.

Let us know what you think below and suggest other topics we can write about!