Skip to content

Call doNothing from mockito in tests for method calls returning void #2016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ internal val ongoingStubbingClassId = BuiltinClassId(
simpleName = "OngoingStubbing",
)

internal val stubberClassId = BuiltinClassId(
canonicalName = "org.mockito.stubbing.Stubber",
simpleName = "Stubber"
)

// We wrap an Object classId in BuiltinClassId
// in order to deal with [CgExpression.canBeReceiverOf]
// when we want to call ANY method on an object.
internal val genericObjectClassId = BuiltinClassId(
canonicalName = "java.lang.Object",
simpleName = "Object"
)

internal val answerClassId = BuiltinClassId(
canonicalName = "org.mockito.stubbing.Answer",
simpleName = "Answer",
Expand Down Expand Up @@ -83,6 +96,19 @@ internal val thenReturnMethodId = builtinMethodId(
arguments = arrayOf(objectClassId)
)

internal val doNothingMethodId = builtinStaticMethodId(
classId = mockitoClassId,
name = "doNothing",
returnType = stubberClassId,
)

internal val whenStubberMethodId = builtinMethodId(
classId = stubberClassId,
name = "when",
returnType = genericObjectClassId,
arguments = arrayOf(objectClassId)
)

// TODO: for this method and other static methods implement some utils that allow calling
// TODO: these methods without explicit declaring class id specification, because right now such calls are too verbose
internal val any = builtinStaticMethodId(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.utbot.framework.codegen.domain.builtin.getMethodId
import org.utbot.framework.codegen.domain.builtin.getTargetException
import org.utbot.framework.codegen.domain.builtin.invoke
import org.utbot.framework.codegen.domain.builtin.newInstance
import org.utbot.framework.codegen.domain.builtin.genericObjectClassId
import org.utbot.framework.codegen.domain.builtin.setAccessible
import org.utbot.framework.codegen.domain.context.CgContext
import org.utbot.framework.codegen.domain.context.CgContextOwner
Expand Down Expand Up @@ -190,13 +191,25 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA

private infix fun CgExpression.canBeReceiverOf(executable: MethodId): Boolean =
when {
// method of the current test class can be called on its 'this' instance
// this executable can be called on its 'this' instance
currentTestClass == executable.classId && this isThisInstanceOf currentTestClass -> true
// method of a class can be called on an object of this class or any of its subtypes

// this executable can be called on an object of this class or any of its subtypes
this.type isSubtypeOf executable.classId -> true

// this executable can be called on builtin type
this.type is BuiltinClassId && this.type in builtinCallersWithoutReflection -> true

else -> false
}

// For some builtin types we need to clarify
// that it is allowed to call an executable without reflection.
//
// This approach is used, for example, to render the constructions with stubs
// like `doNothing().when(entityManagerMock).persist(any())`.
private val builtinCallersWithoutReflection = setOf(genericObjectClassId)

/**
* For Kotlin extension functions, real caller is one of the arguments in JVM method (and declaration class is omitted),
* thus we should move it from arguments to caller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import org.utbot.framework.codegen.domain.builtin.anyLong
import org.utbot.framework.codegen.domain.builtin.anyOfClass
import org.utbot.framework.codegen.domain.builtin.anyShort
import org.utbot.framework.codegen.domain.builtin.argumentMatchersClassId
import org.utbot.framework.codegen.domain.builtin.doNothingMethodId
import org.utbot.framework.codegen.domain.builtin.mockMethodId
import org.utbot.framework.codegen.domain.builtin.mockedConstructionContextClassId
import org.utbot.framework.codegen.domain.builtin.mockitoClassId
import org.utbot.framework.codegen.domain.builtin.thenReturnMethodId
import org.utbot.framework.codegen.domain.builtin.whenMethodId
import org.utbot.framework.codegen.domain.builtin.whenStubberMethodId
import org.utbot.framework.codegen.domain.context.CgContext
import org.utbot.framework.codegen.domain.context.CgContextOwner
import org.utbot.framework.codegen.domain.models.CgAnonymousFunction
Expand Down Expand Up @@ -190,28 +192,39 @@ private class MockitoMocker(context: CgContext) : ObjectMocker(context) {

fun mockForVariable(model: UtCompositeModel, mockObject: CgVariable) {
for ((executable, values) in model.mocks) {
// void method
if (executable.returnType == voidClassId) {
// void methods on mocks do nothing by default
continue
}
val matchers = mockitoArgumentMatchersFor(executable)

when (executable) {
is MethodId -> {
if (executable.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) {
error("Cannot mock method $executable with not accessible parameters" )
if (executable.returnType == voidClassId) {
when (executable) {
// All constructors are considered like void methods, but it is proposed to be changed to test constructors better.
is ConstructorId -> continue
// Sometimes void methods are called explicitly, e.g. annotated with @Mock fields in Spring test classes.
// We would like to mark that this field is used and must not be removed from test class.
// Without `doNothing` call Intellij Idea suggests removing this field as unused.
is MethodId -> {
+mockitoClassId[doNothingMethodId]()[whenStubberMethodId](mockObject)[executable](*matchers)
}
else -> error("Only MethodId and ConstructorId was expected to appear in simple mocker but got $executable")
}
} else {
when (executable) {
is MethodId -> {
if (executable.parameters.any { !it.isAccessibleFrom(testClassPackageName) }) {
error("Cannot mock method $executable with not accessible parameters" )
}

val matchers = mockitoArgumentMatchersFor(executable)
if (!executable.isAccessibleFrom(testClassPackageName)) {
error("Cannot mock method $executable as it is not accessible from package $testClassPackageName")
}
if (!executable.isAccessibleFrom(testClassPackageName)) {
error("Cannot mock method $executable as it is not accessible from package $testClassPackageName")
}

val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray()
`when`(mockObject[executable](*matchers)).thenReturn(executable.returnType, *results)
val results = values.map { variableConstructor.getOrCreateVariable(it) }.toTypedArray()
`when`(mockObject[executable](*matchers)).thenReturn(executable.returnType, *results)
}
else -> error("Only MethodId was expected to appear in simple mocker but got $executable")
}
else -> error("ConstructorId was not expected to appear in simple mocker but got $executable")
}


}
}

Expand Down