loading...
Cover image for Matt's Tidbits #66 - The magic of Kotlin's "reified" keyword

Matt's Tidbits #66 - The magic of Kotlin's "reified" keyword

mpeng3 profile image Matthew Groves Updated on ・3 min read

Last week I called for proper support of JUnit 5 by the Android Gradle Plugin. This time, I want to make sure you know about Kotlin's reified keyword and just how awesome it is!

Recently, I was writing some unit tests for some generic functions, and needed some mockable types I could work with.

So, I created a type like this:

typealias TestObjectID = String

private class TestObject {
  val id: TestObjectID

  fun singleCommand(): Single<String> = Single.never()
  fun maybeCommand(): Maybe<String> = Maybe.never()
  fun completableCommand(): Completable = Completable.never()
}

This let me mock out the various command methods as desired. However, what I discovered (somewhat by accident), is that it was easy to introduce errors from copying/pasting code between tests, since this one object contained all of the various methods I was attempting to test - and if I had mocked the wrong one, it was a little bit confusing to figure out why the test wasn't working.

To make my tests less susceptible to this type of error, I instead split things up like this:

typealias TestObjectID = String

private interface TestObject {
  val id: TestObjectID
}
private interface SingleTestObject : TestObject {
  fun command(): Single<String> = Single.never()
}
private interface MaybeTestObject : TestObject {
  fun command(): Maybe<String> = Maybe.never()
}
...

When set up this way, the method for each object can be named the same (which is nice for readability), and I have to create a SingleTestObject to use the method that returns a Single, a MaybeTestObject to use the method that returns a Maybe, etc. - and I no longer have to worry about mixing up types accidentally.

I am a huge believer in modularized test code that is engineered just as well as the rest of your code. So, instead of each test constructing these different objects manually and mocking them, I like to create a series of helper methods that define various scenarios/common setups. This allows me to reuse that setup code across my tests, and furthermore if the tests need to change to reflect changes in the actual code, it minimizes the number of places I have to fix.

This new setup presented one problem when I wanted to supply a value for the id field of the TestObject interface. I wanted to be able to use a builder pattern, so I could easily configure multiple options in a single sequence. One option would have been to use Kotlin's apply block and a helper method that took an instance of a TestObject, like this:

private fun TestObject.setId(id: TestObjectID) {
  every { this@setId.id } returns id
}

val mockTestObject = createSingleMockTestObject().apply {
  setId("foo")
}

However, I had another idea that I liked even more that uses Kotlin's inline, reified, and where keywords:

private inline fun <reified T> T.withId(id: TestObjectID): T
    where T : TestObject {
  every { this@withId.id } returns id
  return this
}

Why does this work? The reified and inline keywords work together - you can only use reified types if the function is inline. The reified keyword allows this function to return the same type as what was passed in (such as SingleTestObject or MaybeTestObject, even though the operation is performed on the parent class' interface. The where keyword is what allows us to access the id field of TestObject - because this method is only available for objects of type TestObject - so you will not see the withId method appear as a suggestion for other types (such as String).

Update:
A friend a former coworker (@russhwolf ) pointed out that the where statement above is not required. You can achieve the same result by writing the method like this:

private inline fun <reified T: TestObject> T.withId(id: TestObjectID): T {
  every { this@withId.id } returns id
  return this
}

Now, I can use this method in a true builder pattern fashion, and thanks to these special keywords, it will do exactly what I want!

val mockTestObject = createSingleMockTestObject()
  .withId("foo")
  .withSingleValue(Single.just("bar"))

Notice how I can chain the calls together and call the withSingleValue() function, which only applies to SingleTestObject instances, even though the withId() function will operate on any TestObject!

I hope you learned something interesting and useful, and please share your stories about how you have used Kotlin's reified types in the comments below. And, please follow me on Medium if you're interested in being notified of future tidbits.

This tidbit was discovered on April 13, 2020.

Posted on by:

mpeng3 profile

Matthew Groves

@mpeng3

Software engineer with 10+ years of professional experience in C++, C#, Java, and Kotlin.

Discussion

markdown guide