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