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

Time:10-19

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.

CodePudding user response:

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.

CodePudding user response:

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

  • Related