Categories
Kotlin Testing

Mocking Without Mockito

In recent years I have grown to like Mockito, everybody’s favourite and invaluable testing helper, less and less. Using a bytecode-twiddling framework to get your code to behave well in tests makes it hard to reason about the tests, and because Mockito (and other mocking frameworks) tend to use a lot of global state to configure the mocks, it’s possible to break test code by refactorings that on “normal” code are totally safe.

I have adopted a different approach for software I develop that does not use Mockito or any other mocking framework but relies on basic JVM features like interfaces and their implementations.

Let’s assume I have an interface in my application:

interface ThingStore {
  fun getThing(thingId: Int): Thing?
  fun storeThing(thing: Thing)
}

In a test I used to use it like this:

fun `some test`() {
  val thingStore = mock()
  whenever(thingStore.getThing(anyInt()))
    .thenReturn(SomeThing())
  // use thingStore
}

Looks easy enough and doesn’t really warrant worrying about, right? How about this:

fun `some test`() {
  val thingStore = mock()
  whenever(thingStore.getThing(anyInt())).then { invocation ->
    if (invocation.getArgument(0, Int::class.java) == 12) SomeThing1() else SomeThing2()
  }
}

There’s a lot to unpack in those few lines…

Here’s how I’m currently solving this. First, I declare a delegating implementation of the interface:

open class DelegatingThingStore(thingStore: ThingStore) : ThingStore {
  fun getThing(thingId: Int) = thingStore.getThing(thingId)
  fun storeThing(thing: Thing) = thingStore.storeThing(thing)
}

Well, that’s nice and all but if your interface reaches 15 methods, this is a drag, right? Luckily, Kotlin has got your back because it wants you to rely less on inheritance and more on delegation so it has this nifty feature, implementation-by-delegation. Let’s see how it works:

open class DelegatingThingStore(thingStore: ThingStore)
	: ThingStore by thingStore

Wait, what? Yes! That simple expression replaces any number of methods and forwards the implementations right to the given value.

I also need a dummy implementation:

class DummyThingStore : ThingStore {
  fun getThing(thingId: Int): Thing? = null
  fun storeThing(thing: Thing) = Unit
}

val testThingStore: ThingStore = DummyThingStore()

Then I create methods for overriding a single method in a ThingStore:

fun ThingStore.overrideGetThing(override: (thingId: Int) -> Thing?): ThingStore = object : DelegatingThingStore(this) {
  override fun getThing(thingId: Int) = override(thingId)
}

fun ThingStore.overrideStoreThing(override: (thing: Thing) -> Unit): ThingStore = object : DelegatingThingStore(this) {
  override fun storeThing(thing: Thing) = override(thing)
}

Now I can use it in a test like this:

fun `some test`() {
  val thingStore = testThingStore
    .overrideGetThing { if (it == 12) SomeThing1() else SomeThing2() }
  // use the thing store
}

So the way to use it is to start with a random implementation of a ThingStore (could even be a real one!) and override the methods I need for the test. You could even combine this method with a ThingStore mock created by Mockito which would allow you to get away without the dummy implementation as that is now provided by Mockito.

Let’s quickly compare the two approaches. The Mockito-based one:

  • ➖ Uses bytecode manipulation to do its thing.
  • ➖ Makes setting up objects error-prone due to global state.
  • ➖ Is nearly impossible to debug.
  • ➕ Is very popular.
  • ➕ Can also mock arbitrary classes.

The delegation-based approach:

  • ➖ Requires everything that needs to be mocked to be an interface.
  • ➖ Requires boiler plate code for every interface that needs to be mocked.
  • ➖ Is (probably) probably the reason why Mockito was invented in the first place.
  • ➕ Uses only standard Kotlin/Java features.
  • ➕ Is easy to debug because of that.
  • ➕ Looks nice. 😀

Now, there are still situations in which Mockito is quite irreplacable such as when testing with legacy APIs that are beyond your control; in these cases Mockito to the rescue!

Also, this is basically what everybody had to do before Mockito (et. al.) entered the scene so it feels kind of weird to post it like it actually was anything new. Interfaces and specialized implementations for tests, it doesn’t get more oldschool than this!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.