Unit testing protected lifecycle methods with Kotlin

https://www.flickr.com/photos/frosch50/13186200564

Developers often discuss where to stop unit testing. Should we only test the Java level and stop when we reach an Android class? We need Espresso tests for those, right?

When I do test driven development I don’t want to stop. Stopping slows me down. And even executing a UI test means slowing me down.

So when writing an Android application, or adding a new feature to one, there is code I want to test that lives in the Activity. Of course this code should be very limited. All code should be moved out into specific classes like Presenters. But still there is code left. Even when applying my favourite model: MVVM with Android data binding the Activity still has to bind the model and the layout together. Or it might forward certain life cycle events.

And it’s not a big issue as for quite some time we can unit test a lot of android classes even without the need for Robolectric. You can just call the onCreate() method of an Activity from pure java unit test and guess what will happen? Nothing! As all code is removed as are all the final modifiers. See for yourself, just open the build/generated folder and you’ll find a mockable-android jar file that is generated on the fly for your tests.

So let’s assume I have a test like this:

@Test
fun `should inflate layout`() {
val tested = spy(MainActivity())
tested.onCreate(null)
verify(tested).setContentView(R.layout.activity_main)
}

Here we want to make sure the Activity inflated a specific layout (and I am also checking that onCreate can handle a null bundle). This is something I would write when doing TDD as I have to force myself to write the test that forces me to write specific code.

But when you try to write this, you will see :

Cannot access ‘onCreate’: it is protected in ‘MainActivity’

This totally makes sense.

In Java it would be easy to trick the system. Just have the test class in the same package as the Activity we are testing.

Kotlin made our life a bit harder, it does not accept this anymore. And it’s correct, this method is marked as protected to override it from subclasses but it’s not accessible from every class that sits in the same package of one of the subclasses.

But how do we solve this for the test?

I could change the visibility of those methods in my subclass. But I would do something only for the test. Something we should avoid.

There is a better way: the package rule still applies for where the method was declared so in the original Activity class. So everyone in the android.app package can still create onCreate().

Of course you don’t want to move all your tests into that system package. So what we need is a bridge and as we use Kotlin we can build an elegant one:

package android.app

import android.os.Bundle

fun Activity.onCreate(bundle: Bundle?) = this.onCreate(bundle)

This snippet defines an extension function with the same name as our lifecycle method and then just calls it. Remember extension functions are just static functions so for the JVM, there is no name conflict.

All we need in our test is to import this method and keep the rest of the code as is:

import android.app.onCreate
@Test
fun `should inflate layout`() {
val tested = spy(MainActivity())
tested.onCreate(null)
verify(tested).setContentView(R.layout.activity_main)
}

Now the onCreate you are calling is using our little indirection without hurting the readability of the test.

If you are too lazy to write this yourself, feel free to grab my version, that implements a bridge for all the protected methods including onActivityResult:

repositories {
maven {url "https://jitpack.io"}
}
testImplementation 'com.github.dpreussler:android-tdd-utils:v0.1'

PS: you will run into some issues when trying to call onCreate() of AppCompatActivity. Reason: the mockable jar is not helping there. We will look into how to solve this in a future blog post. Stay tuned.