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.
Top comments (0)