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.