You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 10:58:10