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

无需Mockito/MockK等框架,如何验证单元测试中方法的调用?

脱离Mock框架验证无返回值函数调用的手动实现方案

核心思路是自定义测试用的Toaster实现类,在类内部维护每个函数的调用记录(调用次数、传入参数等),通过自带的断言逻辑完成验证,完全不需要依赖MockK、Mockito这类框架。

方式一:为每个方法单独维护调用记录(直观易读)

这种方案针对每个接口方法单独记录调用信息,验证逻辑更直观:

class TestToaster: Toaster {
    // 分别记录不同重载方法的调用参数
    private val successStringCalls = mutableListOf<String>()
    private val successIntCalls = mutableListOf<Int>()
    private val errorStringCalls = mutableListOf<String>()
    private val errorIntCalls = mutableListOf<Int>()

    override fun showSuccessMessage(message: String) {
        successStringCalls.add(message)
    }

    override fun showSuccessMessage(message: Int) {
        successIntCalls.add(message)
    }

    override fun showErrorMessage(message: String) {
        errorStringCalls.add(message)
    }

    override fun showErrorMessage(message: Int) {
        errorIntCalls.add(message)
    }

    // 验证带String参数的showSuccessMessage调用情况
    fun verifyShowSuccessMessageCalled(times: Int = 1, message: String? = null) {
        val actualCount = message?.let { msg ->
            successStringCalls.count { it == msg }
        } ?: successStringCalls.size

        check(actualCount == times) {
            "Expected showSuccessMessage(String) to be called $times times" +
                    (message?.let { " with message '$it'" } ?: "") +
                    ", but was called $actualCount times"
        }
    }

    // 验证带Int参数的showSuccessMessage调用情况
    fun verifyShowSuccessMessageCalled(times: Int = 1, message: Int? = null) {
        val actualCount = message?.let { msg ->
            successIntCalls.count { it == msg }
        } ?: successIntCalls.size

        check(actualCount == times) {
            "Expected showSuccessMessage(Int) to be called $times times" +
                    (message?.let { " with message '$it'" } ?: "") +
                    ", but was called $actualCount times"
        }
    }

    // 同理实现错误提示方法的验证
    fun verifyShowErrorMessageCalled(times: Int = 1, message: String? = null) {
        val actualCount = message?.let { msg ->
            errorStringCalls.count { it == msg }
        } ?: errorStringCalls.size

        check(actualCount == times) {
            "Expected showErrorMessage(String) to be called $times times" +
                    (message?.let { " with message '$it'" } ?: "") +
                    ", but was called $actualCount times"
        }
    }

    fun verifyShowErrorMessageCalled(times: Int = 1, message: Int? = null) {
        val actualCount = message?.let { msg ->
            errorIntCalls.count { it == msg }
        } ?: errorIntCalls.size

        check(actualCount == times) {
            "Expected showErrorMessage(Int) to be called $times times" +
                    (message?.let { " with message '$it'" } ?: "") +
                    ", but was called $actualCount times"
        }
    }

    // 重置调用记录,方便复用测试实例
    fun reset() {
        successStringCalls.clear()
        successIntCalls.clear()
        errorStringCalls.clear()
        errorIntCalls.clear()
    }
}

使用示例

在测试代码中初始化实例、执行业务逻辑后,直接调用验证方法:

// 初始化测试用Toaster
val toaster = TestToaster()

// 执行被测试的业务逻辑(比如触发showSuccessMessage的操作)
userService.handleLoginSuccess(toaster)

// 验证方法被调用,且参数匹配
toaster.verifyShowSuccessMessageCalled(message = "登录成功")

// 验证方法被调用2次
toaster.verifyShowSuccessMessageCalled(times = 2)

方式二:通用调用记录方案(适合多方法场景)

如果接口方法较多,可以用统一的结构记录所有调用,减少重复代码:

// 定义数据类存储调用信息
data class MethodCall(
    val methodName: String,
    val arguments: List<Any?>
)

class TestToaster: Toaster {
    private val allCalls = mutableListOf<MethodCall>()

    override fun showSuccessMessage(message: String) {
        allCalls.add(MethodCall("showSuccessMessage", listOf(message)))
    }

    override fun showSuccessMessage(message: Int) {
        allCalls.add(MethodCall("showSuccessMessage", listOf(message)))
    }

    override fun showErrorMessage(message: String) {
        allCalls.add(MethodCall("showErrorMessage", listOf(message)))
    }

    override fun showErrorMessage(message: Int) {
        allCalls.add(MethodCall("showErrorMessage", listOf(message)))
    }

    // 通用验证方法,支持指定方法名、调用次数和参数
    fun verifyMethodCalled(methodName: String, times: Int = 1, arguments: List<Any?>? = null) {
        val matchingCalls = if (arguments != null) {
            allCalls.filter { it.methodName == methodName && it.arguments == arguments }
        } else {
            allCalls.filter { it.methodName == methodName }
        }

        check(matchingCalls.size == times) {
            "Expected method '$methodName' to be called $times times" +
                    (arguments?.let { " with arguments $it" } ?: "") +
                    ", but was called ${matchingCalls.size} times"
        }
    }

    fun reset() = allCalls.clear()
}

使用示例

val toaster = TestToaster()

// 执行业务逻辑
orderService.handleOrderFailure(toaster)

// 验证带Int参数的showErrorMessage被调用1次
toaster.verifyMethodCalled("showErrorMessage", arguments = listOf(500))

// 验证showSuccessMessage从未被调用
toaster.verifyMethodCalled("showSuccessMessage", times = 0)

核心原理

所有验证逻辑依赖Kotlin自带的check函数:当实际调用情况不符合预期时,check会抛出IllegalStateException,测试框架(如JUnit、Spek)会自动捕获该异常并标记测试失败。这种方式完全不需要引入额外依赖,实现轻量且可控的测试验证。

内容的提问来源于stack exchange,提问作者Sermilion

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 21:01:22