我正在使用 EasyMock 和 Kotlin。我尝试模拟一个示例类。
我不断收到的问题是
anyObject
,无论是否有特定的类,都会抛出 NullPointerException
,因为 Kotlin 在类型方面比 Java 更严格。
java.lang.NullPointerException:anyObject(Logger::class.java) 不能为 null
我使用简单的类实现运行的测试示例如下。
基于EasyMock的测试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()
}
}
我之前也遇到过与
anyString
类似的问题,但是通过anyString
的功能扩展解决了这个问题,但是对于anyObject
我无法想出类似的实现。
当前的测试有什么方法可以使其在没有
NullPointerException
功能或其他替代方案的情况下工作吗?
这是一个有趣的问题。因为 Kotlin 无法接受传递给无法接收 null 的方法的 null,所以它会变得疯狂。我做了一些阅读和实验。看来唯一的办法就是做这样的事情。
object Helper {
fun <T> anyObject(item: Class<T>, result: T): T {
EasyMock.anyObject(item)
return result
}
fun anyString(): String {
EasyMock.anyString()
return ""
}
}
和
@Test
fun `should test logger implementation`() {
val result : Logger = mock(Logger::class.java)
loggerService.info(Helper.anyObject(Logger::class.java, result), Helper.anyString())
replay(loggerService)
myService.`generate different logs based on incoming numbers`(0)
verify(loggerService)
}
在这种情况下,
anyString
和anyObject
不再返回null
。我需要在参数中传递 result
而不是做
fun <T> anyObject(item: Class<T>): T {
EasyMock.anyObject(item)
return mock(item)
}
因为你无法在匹配器中创建模拟。它让 EasyMock 疯狂。
遗憾的是,当使用 EasyMock 5.2.0 和 Java 21 执行此操作时,模拟下的拦截器似乎无法正常工作。我们得到一个
java.lang.IllegalStateException: matcher calls were used outside expectations
。我不知道为什么,但这很可能是因为 Kotlin 为 LoggerService
生成了一个奇怪的类。如果您有相同的结果,您可以提交 EasyMock bug。