Hello dev expert! Are you up for a challenge?

Maciej Najbar
5 min readOct 6, 2023

--

I created a framework for Test Doubles without using reflection. Are you up for a challenge to create your own?

I’ve been working with Mockito for a long time, before that I used to use JMock which I found very interesting. Both libraries use reflection to override the methods. I wanted to know if I’m able to create a mocking tool without using reflection and get a pleasant API. Here’s the result.

interface Example {
fun get(): String
fun getInt(): Int?
}

@Test
fun `test stub`() {
val expected = "stubbed"
val anotherExpected = 44
val example: Example = StubExample {
on { get() }.thenReturn(expected)
on { getInt() }.thenReturn(anotherExpected)
}

assertEquals(expected, example.get())
assertEquals(anotherExpected, example.getInt())
}

@Test
fun `test spy`() {
val expected = "stubbed"
val anotherExpected = 44
val example = StubExample {
on { get() }.thenReturn(expected)
on { getInt() }.thenReturn(anotherExpected)
}

example.get()

assertEquals(expected, example.mock.lastValues["get"])
assertEquals(null, example.mock.lastValues["getInt"])
}

@Test
fun `test mock`() {
val expected = "stubbed"
val anotherExpected = 44
val example = StubExample {
on { get() }.thenReturn(expected).expect(expected)
on { getInt() }.thenReturn(anotherExpected)
}

example.get()

example.mock.verify()
}

If you’re interested how I did it read the article.

What Are Testing Doubles

Before we jump into the implementation it’s good to know about the theory of Test Doubles. They were introduced Gerard Meszaros. He identified several terms that we use for testing.

  • Fake — a simple implementation that emulates the real object behavior. This is the least used double because Fakes can get very complicated with time and may result with buggy behavior that satisfies tests.
  • Dummy — an object that is used for parameters. This object does not play any part in the test it’s present in order to make the code compile and run.
  • Stub — an object with predefined behavior to satisfy the path we currently test e.g. what happens for error case.
  • Spy — an object that allows us to get the last values.
  • Mock — an object that is used for verifying set expectations.

These may not be the exact definitions that you can find on the Internet but the way I use them in testing.

Test Doubles are in hierarchy except for Fake which is a standalone object with its own implementation. Mock is a Spy. Spy is a Stub. Stub is a Dummy.

Dummy

It’s a simple implementation that returns default values.

open class Dummy : Example {
override fun get(): String = ""
override fun getInt(): Int? = null
}

Stub

This object allows us to define a specific behavior per test.

open class Stub(
private val onGet: () -> String,
private val onGetInt: () -> Int?,
) : Dummy() {
override fun get(): String = onGet()
override fun getInt(): Int? = onGetInt()
}

Spy

This this object keeps the values that were used.

open class Spy(
onGet: () -> String,
onGetInt: () -> Int?,
) : Stub(onGet, onGetInt) {

val lastValues = mutableMapOf<String, Any?>()

override fun get(): String {
val result = super.get()
lastValues["get"] = result
return result
}

override fun getInt(): Int? {
val result = super.getInt()
lastValues["getInt"] = result
return result
}
}

Mock

It allows us us to verify if values returned by a used function is as expected.

class Mock(
onGet: () -> String,
onGetInt: () -> Int?,
private val expectedGet: String,
private val expectedGetInt: Int?,
) : Spy(onGet, onGetInt) {
init {
lastValues.apply {
put("get", unintialized)
put("getInt", unintialized)
}
}

fun verify() {
val verify: (Any?, String) -> Unit = { expected, key ->
val lastValue = lastValues[key]
if (lastValue != unintialized) {
assert(expected == lastValue) { "Expected: $expected, Actual: $lastValue" }
}
}
verify(expectedGet, "get")
verify(expectedGetInt, "getInt")
}

companion object {
private val unintialized = Any()
}
}

Let’s see this Mock implementation in practice.

@Test
fun mock() {
val example = Mock(
onGet = { "result" },
onGetInt = { 44 },
expectedGet = "result",
expectedGetInt = 44,
)

example.get()

example.verify()
}

Refactoring

Creating this entire hierarchy each time we want to test something seem unreasonable. Let’s see what we can do about it.

An old saying states “code to abstraction, not implementation”. What it means is that we should always think about intention and find a way to satisfy that with implementation.

As you could see in the beginning of this article my intention is to stub in a lambda like this.

val example = StubExample {
on { get() }.thenReturn(expected)
on { getInt() }.thenReturn(anotherExpected)
}

After small refactoring, I get this state.

open class StubExample(
private val onGet: () -> String,
private val onGetInt: () -> Int?,
stubbing: Stub.() -> Unit = {},
) : Dummy() {

private var isStubbing = false
private val stubs = mutableMapOf<String, Any?>()

init {
stubbing(this)
}

override fun get(): String = withStubName("get") {
stubs[it] as? String ?: ""
}

override fun getInt(): Int? = withStubName("getInt") {
stubs[it] as? Int
}

fun <T> withStubName(key: String, block: (String) -> T): T {
if (isStubbing) {
stubs[key] = uninitialized
}

return block(key)
}

fun on(block: Example.() -> Unit): OngoingStub {
isStubbing = true
block(this)
return OngoingStub()
}

inner class OngoingStub {
fun thenReturn(expected: Any?) {
isStubbing = false
stubs.entries
.first { it.value == uninitialized }
.apply { stubs[key] = expected }
}
}
private companion object {
val uninitialized = Any()
}
}

I can remove the onGet and onGetInt since they are not used anymore and I can get more generic. The last stage of my refactoring looks like the following.

class Mock<T1>(
private val instance: T1,
) {
private var isStubbing = false

val stubs = mutableMapOf<String, Any?>()
val lastValues = mutableMapOf<String, Any?>()
val expectations = mutableMapOf<String, Any?>()

fun on(block: T1.() -> Unit): OngoingStub {
isStubbing = true
block(instance)
return OngoingStub()
}

fun <T2> withStubName(name: String, block: (T2?) -> T2): T2 {
if (isStubbing) {
stubs[name] = uninitialized
expectations[name] = uninitialized
}

val result = block(stubs[name].takeUnless { it == uninitialized } as? T2)
lastValues[name] = result

return result
}

fun verify() {
expectations.entries
.filterNot { it.value == uninitialized }
.forEach { assert(lastValues[it.key] == expectations[it.key]) { "Expected: ${expectations[it.key]}, Actual: ${lastValues[it.key]}" } }
}

inner class OngoingStub {
fun thenReturn(obj: Any?): OngoingStub {
isStubbing = false
stubs.entries
.first { it.value == uninitialized }
.apply { stubs[key] = obj }

return this
}

fun expect(obj: Any?) {
expectations.entries
.first { it.value == uninitialized }
.apply { expectations[key] = obj }
}
}

companion object {
private val uninitialized = Any()
}
}

I encourage you to go through this refactoring process yourself. This generic form for Mock (since the refactoring went on from Stub to Mock) can be used in a composition which means the usage looks like following.

@Test
fun `test mock`() {
val expected = "stubbed"
val anotherExpected = 44
val example = StubExample {
on { get() }.thenReturn(expected).expect(expected)
on { getInt() }.thenReturn(anotherExpected)
}

example.get()

example.mock.verify()
}

class StubExample(
mocking: Mock<Example>.() -> Unit = {},
) : Example {

val mock = Mock<Example>(this)

init { mocking(mock) }

override fun get(): String = mock.withStubName("get") { it ?: "" }

override fun getInt(): Int? = mock.withStubName("getInt") { it }
}

The Bottom Line

There are languages that do not support the full reflection (e.g. Swift) that encouraged me to write a generic form of mocking tool to make it easier for everyone. Feel free to use it and share it if you like. If you think there is a better way to do it please comment below.

If you liked this article, you might also like others. Follow me on Medium to get more content like this.

--

--