I am using EasyMock with Kotlin. There is a sample class that I try mocking.
The issue I continuously receive is that anyObject, independently with or without a specific class, throws NullPointerException due to Kotlin being stricter than Java with typing.
java.lang.NullPointerException: anyObject(Logger::class.java) must not be null
Examples of the tests I run with simple class implementations are below.
EasyMock-based test MyServiceMockTest.kt:
import org.easymock.EasyMock.anyObject
import org.easymock.EasyMock.anyString
import org.easymock.EasyMock.replay
import org.easymock.EasyMock.verify
import org.easymock.EasyMockExtension
import org.easymock.Mock
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.slf4j.Logger
@ExtendWith(EasyMockExtension::class)
class MyServiceMockTest {
@Mock
private lateinit var loggerService: LoggerService
private lateinit var myService: MyService
@BeforeEach
fun setUp() {
myService = MyService(loggerService)
}
@Test
fun `should test logger implementation`() {
loggerService.info(anyObject(Logger::class.java), anyString())
replay(loggerService)
myService.`generate different logs based on incoming numbers`(0)
verify(loggerService)
}
}
MyService.kt:
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class MyService @Autowired constructor(val loggerService: LoggerService) {
private val logger = LoggerFactory.getLogger(MyService::class.java)
companion object {
const val BREADCRUMB_ID = "fd8f6ac2-8d27-11ee-b9d1-0242ac120002"
}
fun `generate different logs based on incoming numbers`(num: Int) {
when (num) {
0 ->
loggerService.info(
logger = logger,
breadcrumbId = BREADCRUMB_ID
)
1 ->
loggerService.info(
logger = logger,
breadcrumbId = BREADCRUMB_ID,
events = listOf("Log a single message")
)
2 ->
loggerService.info(
logger = logger,
breadcrumbId = BREADCRUMB_ID,
events = listOf("Log a message"),
params = mapOf("num" to num)
)
3 ->
loggerService.warn(
logger = logger,
breadcrumbId = BREADCRUMB_ID,
params = mapOf("num warnings" to num)
)
else -> {
loggerService.info(
logger = logger,
breadcrumbId = BREADCRUMB_ID
)
loggerService.warn(
logger = logger,
breadcrumbId = BREADCRUMB_ID
)
loggerService.error(
logger = logger,
breadcrumbId = BREADCRUMB_ID
)
}
}
}
}
LoggerService.kt:
import org.slf4j.Logger
import org.springframework.boot.logging.LogLevel
import org.springframework.boot.logging.LogLevel.ERROR
import org.springframework.boot.logging.LogLevel.INFO
import org.springframework.boot.logging.LogLevel.WARN
import org.springframework.stereotype.Service
/**
* As this LoggerService unique per service,
* be it one of the services in a monolith
* or one of isolated services in microservice architecture
* it has an extra field <code>breadcrumbId</code>, so
* all the messages can be traced by this ID.
*/
@Service
open class LoggerService {
fun info(logger: Logger, breadcrumbId: String, events: List<Any>? = null, params: Map<Any, Any?>? = null) {
log(INFO, logger, breadcrumbId, events, params)
}
fun error(logger: Logger?, breadcrumbId: String?, events: List<Any>? = null, params: Map<Any, Any?>? = null) {
log(ERROR, logger!!, breadcrumbId!!, events, params)
}
fun warn(logger: Logger, breadcrumbId: String, events: List<Any>? = null, params: Map<Any, Any?>? = null) {
log(WARN, logger, breadcrumbId, events, params)
}
/**
* The implementation is limited, it does not include TRACE, DEBUG modes.
*/
private fun log(level: LogLevel, logger: Logger, breadcrumbId: String, events: List<Any>? = null, params: Map<Any, Any?>? = null) {
/**
* Builder behavior mimics MDC Logger.
* It allows to have a greater flexibility, than some other available solutions.
*/
val message = LogMessage(breadcrumbId)
.addParams("events", events)
.addParams("params", params)
.build()
when (level) {
ERROR -> logger.error(message)
WARN -> logger.warn(message)
else -> logger.info(message)
}
}
}
/**
* The implementation of this class can be further extended.
*
* Here is a simple reference implementation that can be used as it is.
*/
open class LogMessage {
private val builder: StringBuilder
constructor(breadcrumbId: String){
builder = StringBuilder("[$breadcrumbId]")
}
fun addParams(name: Any, value: Any?): LogMessage {
if (value != null) {
builder.append(", $name: $value")
}
return this
}
fun build(): String {
return builder.toString()
}
}
I had a similar issue with anyString before, but it got resolved by the function extension of anyString but for anyObject I cannot come up with a similar implementation.
Is there any way the current test makes it work without NullPointerException with function or other alternatives?
It's an interesting issue. Because Kotlin can't accept a null passed to a method that can't receive one, it goes crazy. I did some reading and experiment. It seems that the only way is to do something like this.
and
In this case,
anyStringandanyObjectare not returningnullanymore. I need to passresultin parameter instead of doingbecause you can't create a mock in a matcher. It drives EasyMock crazy.
Sadly, when doing this with EasyMock 5.2.0 and Java 21, it seems the interceptor under the mock is not working as it should. We get a
java.lang.IllegalStateException: matcher calls were used outside expectations. I don't know why but it's most probably because Kotlin generates a strange class forLoggerService. If you have the same result, you can file an EasyMock bug.