Kotlin中使用EasyMock调用anyObject()报空指针,如何解决?
Kotlin中EasyMock anyObject()空指针问题解决
问题场景
用Kotlin结合EasyMock做单元测试时,调用anyObject(Logger::class.java)触发NullPointerException,报错:
java.lang.NullPointerException: anyObject(Logger::class.java) must not be null
本质是Kotlin的非空类型检查不允许null赋值给非空参数,而EasyMock的anyObject()内部返回的是null。之前anyString()的问题靠扩展函数解决了,但anyObject没法直接套用相同逻辑。
相关代码示例
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() } }
可行解决方法
方法1:自定义泛型扩展函数(推荐)
写一个泛型的any()函数,内部调用anyObject()并强制转换,同时用注解消除非空警告:
import org.easymock.EasyMock.anyObject @Suppress("UNCHECKED_CAST") private fun <T> any(): T { anyObject<T>() return null as T }
然后在测试方法里替换原来的anyObject调用:
@Test fun `should test logger implementation`() { loggerService.info(any<Logger>(), anyString()) replay(loggerService) myService.`generate different logs based on incoming numbers`(0) verify(loggerService) }
这个方法和之前解决anyString问题的思路一致,适配所有非空类型的参数匹配。
方法2:使用EasyMock的isA()方法
如果只需要匹配特定类型的实例,isA()会返回一个动态代理的非null实例,刚好符合Kotlin的非空要求:
import org.easymock.EasyMock.isA @Test fun `should test logger implementation`() { loggerService.info(isA(Logger::class.java), anyString()) replay(loggerService) myService.`generate different logs based on incoming numbers`(0) verify(loggerService) }
注意:isA()要求传入的参数必须是该类型的实例,而anyObject()是匹配任意对象(包括null),如果你的场景允许用类型匹配,这个方法更简洁。
方法3:修改原方法参数为可空(不推荐)
除非业务上允许logger为null,否则不建议改动生产代码。如果要改,把LoggerService的info、warn方法的logger参数改为可空类型,然后内部做非空校验:
fun info(logger: Logger?, breadcrumbId: String, events: List<Any>? = null, params: Map<Any, Any?>? = null) { requireNotNull(logger) { "Logger cannot be null" } log(INFO, logger, breadcrumbId, events, params) } fun warn(logger: Logger?, breadcrumbId: String, events: List<Any>? = null, params: Map<Any, Any?>? = null) { requireNotNull(logger) { "Logger cannot be null" } log(WARN, logger, breadcrumbId, events, params) }
改完之后原来的anyObject(Logger::class.java)调用就不会触发NPE了,但会破坏原有代码的非空约定,谨慎使用。
内容的提问来源于stack exchange,提问作者Dmytro Chasovskyi
相关产品推荐
相关产品推荐

