Home > Mobile >  Unit Testing: Verify method a was called without testing frameworks
Unit Testing: Verify method a was called without testing frameworks

Time:10-17

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 a simple Matcher:

inline fun <reified T> any(): T {
  return when (T::class) {
    Int::class -> -1 as T
    Long::class -> -1L as T
    Byte::class -> -1 as T
    Short::class -> -1 as T
    Float::class -> -1.0 as T
    Double::class -> -1.0 as T
    Char::class -> 'a' as T
    String:: class -> "a" as T
    Boolean::class -> true as T
    else -> {
      T::class.constructors.first().call()
    }
  }
}

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. Here is the complete class:

open class TestToaster : Toaster {

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

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

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

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

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

  private fun addCall(key: String, param: String? = null) {
    val currentValue = callCount.getOrDefault(key, 0)
    callCount[key] = currentValue   1
  }

  private class InnerToaster(val toaster: Toaster, val times: Int) : TestToaster() {

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

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

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

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

    private fun check(functionName: String) {
      val actualTimes = getActualCallCount(functionName)
      if (actualTimes != times) {
        throw IllegalStateException(
          "$functionName expected to be called $times, but actual was $actualTimes"
        )
      }
    }

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

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

This version only considers call count, but you can imagine that it is easy to add another map to store parameters for function and check those as well.

  • Related