Unit Testing: Verify that a method was called, without testing frameworks like Mockito or MockK

235 Views Asked by At

Not using testing frameworks like MockK or Mockito seems to be becoming more and more popular. I decided to try this approach. So far so good, returning fake data is simple. But how do I verify that a function (that does not return data) has been called? Imagine having a calss like this:

class TestToaster: Toaster {

  override fun showSuccessMessage(message: String) {
    throw UnsupportedOperationException()
  }

  override fun showSuccessMessage(message: Int) {
    throw UnsupportedOperationException()
  }

  override fun showErrorMessage(message: String) {
    throw UnsupportedOperationException()
  }

  override fun showErrorMessage(message: Int) {
    throw UnsupportedOperationException()
  }
}

With MockK I would do

verify { toaster.showSuccessMessage() }

I do not want to reinvent a wheel so decided to ask. Finding anything on Google seems to be very difficult. Since this is a thing, I assume the point would be to totally remove mocking libraries and everything can be done without them.

2

There are 2 best solutions below

0
On

A lot of people seem to be suggesting the very straight forward solution for this, which totally makes sense. I decided to go a bit fancy and achieve this syntax:

verify(toaster = toaster, times = 1).showErrorMessage(any<String>()).

I created simple Matchers:

inline fun <reified T> anyObject(): T {
  return T::class.constructors.first().call()
}

inline fun <reified T> anyPrimitive(): T {
  return when (T::class) {
    Int::class -> Int.MIN_VALUE as T
    Long::class -> Long.MIN_VALUE as T
    Byte::class -> Byte.MIN_VALUE as T
    Short::class -> Short.MIN_VALUE as T
    Float::class -> Float.MIN_VALUE as T
    Double::class -> Double.MIN_VALUE as T
    Char::class -> Char.MIN_VALUE as T
    String:: class -> "io.readian.readian.matchers.strings" as T
    Boolean::class -> false as T
    else -> {
      throw IllegalArgumentException("Not a primitive type ${T::class}")
    }
  }
}

Added a map to store call count for each method to my TestToaster where the key is the name of the function and value is the count:

private var callCount: MutableMap<String, Int> = mutableMapOf()

Whenever a function gets called I increase current call count value for a method. I get current method name through reflection

val key = object {}.javaClass.enclosingMethod?.name + param::class.simpleName
addCall(key)

In oder to achieve the "fancy" syntax, I created inner subcalss for TestToaster and a verify function:

fun verify(toaster: Toaster , times: Int = 1): Toaster {
  return TestToaster.InnerToaster(toaster, times)
}

That function sends current toaster instance to the inner subclass to create new instance and returns it. When I call a method of the subclass in my above syntax, the check happens. If the check passes, nothing happens and test is passed, if conditions not met - and exception is thrown.

To make it more general and extendable I created this interface:

interface TestCallVerifiable {
  var callCount: MutableMap<String, Int>
  val callParams: MutableMap<String, CallParam>

  fun addCall(key: String, vararg param: Any) {
    val currentCountValue = callCount.getOrDefault(key, 0)
    callCount[key] = currentCountValue + 1
    callParams[key] = CallParam(param.toMutableList())
  }

  abstract class InnerTestVerifiable(
    private val outer: TestCallVerifiable,
    private val times: Int = 1,
  ) {

    protected val params: CallParam = CallParam(mutableListOf())

    protected fun check(functionName: String) {
      val actualTimes = getActualCallCount(functionName)
      if (actualTimes != times) {
        throw IllegalStateException(
          "$functionName expected to be called $times, but actual was $actualTimes"
        )
      }
      val callParams = outer.callParams.getOrDefault(functionName, CallParam(mutableListOf()))
      val result = mutableListOf<Boolean>()
      callParams.values.forEachIndexed { index, item ->
        val actualParam = params.values[index]
        if (item == params.values[index] || (item != actualParam && isAnyParams(actualParam))) {
          result.add(true)
        }
      }
      if (params.values.isNotEmpty() && !result.all { it } || result.isEmpty()) {
        throw IllegalStateException(
          "$functionName expected to be called with ${callParams.values}, but actual was with ${params.values}"
        )
      }
    }

    private fun isAnyParams(vararg param: Any): Boolean {
      param.forEach {
        if (it.isAnyPrimitive()) return true
      }
      return false
    }

    private fun getActualCallCount(functionName: String): Int {
      return outer.callCount.getOrDefault(functionName, 0)
    }
  }

  data class CallParam(val values: MutableList<Any> = mutableListOf())
}

Here is the complete class:

open class TestToaster : TestCallVerifiable, Toaster {

  override var callCount: MutableMap<String, Int> = mutableMapOf()
  override val callParams: MutableMap<String, TestCallVerifiable.CallParam> = mutableMapOf()

  override fun showSuccessMessage(message: String) {
    val key = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
    addCall(key, message)
  }

  override fun showSuccessMessage(message: Int) {
    val key = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
    addCall(key, message)
  }

  override fun showErrorMessage(message: String) {
    val key = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
    addCall(key, message)
  }

  override fun showErrorMessage(message: Int) {
    val key = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
    addCall(key, message)
  }

  private class InnerToaster(
    verifiable: TestCallVerifiable,
    times: Int,
  ) : TestCallVerifiable.InnerTestVerifiable(
    outer = verifiable,
    times = times,
  ), Toaster {

    override fun showSuccessMessage(message: String) {
      params.values.add(message)
      val functionName = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
      check(functionName)
    }

    override fun showSuccessMessage(message: Int) {
      params.values.add(message)
      val functionName = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
      check(functionName)
    }

    override fun showErrorMessage(message: String) {
      params.values.add(message)
      val functionName = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
      check(functionName)
    }

    override fun showErrorMessage(message: Int) {
      params.values.add(message)
      val functionName = object {}.javaClass.enclosingMethod?.name + message::class.simpleName
      check(functionName)
    }
  }

  companion object {
    fun verify(toaster: Toaster, times: Int = 1): Toaster {
      return InnerToaster(toaster as TestCallVerifiable, times)
    }
  }
}

I have not tested this extensively and it will evolve with time, but so far it works well for me.

I also wrote an article about this on Medium: https://sermilion.medium.com/unit-testing-verify-that-a-method-was-called-without-testing-frameworks-like-mockito-or-mockk-433ef8e1aff4

0
On

The old school way to do it before any appearance of the mocking library is to manually create an implementation that is just for testing . The test implementation will store how an method is called to some internal state such that the testing codes can verify if a method is called with expected parameters by checking the related state.

For example , a very simple Toaster implementation for testing can be :

public class MockToaster implements Toaster {
    
    public String showSuccesMessageStr ;
    public Integer showSuccesMessageInt;

    public String showErrorMessageStr;
    public Integer showErrorMessageInt;
        
    public void showSuccessMessage(String msg){
        this.showSuccesMessageStr = msg;
    }

    public void showSuccessMessage(Integer msg){
        this.showSuccesMessageInt = msg;
    }

    public void showErrorMessage(String msg){
        this.showErrorMessageStr = msg;
    }

    public void showErrorMessage(Integer msg){
        this.showErrorMessageInt = msg;
    }
}

Then in your test codes , you configure the object that you want to test to use MockToaster. To verify if it does really call showSuccessMessage("foo") , you can then assert if its showSuccesMessageStr equal to foo at the end of the test.