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

如何单元测试Kotlin挂起函数?基于MVP+UseCase架构场景

完成Presenter中loadPreviews()的BDD风格单元测试

首先咱们明确这个测试要验证的核心行为:

  • 当Presenter调用loadPreviews()时,会触发previewsUseCase.getPreviews()的挂起调用
  • 拿到UseCase返回的结果后,会调用View的showPreviews()方法传递正确的数据

接下来一步步完成测试:

1. 完善BDD辅助函数

你已经写了givenSuspended的雏形,咱们补全这个适配挂起函数的Stub工具:

import kotlinx.coroutines.runBlocking
import org.mockito.BDDMockito

// 适配挂起函数的BDDMockito given扩展
fun <T> givenSuspended(block: suspend () -> T) = BDDMockito.given(runBlocking { block() })

// 给Stub添加简洁的返回值语法
infix fun <T> BDDMockito.BDDMyOngoingStubbing<T>.willReturnSuspended(value: T) = this.willReturn(value)

2. 完整测试类示例

假设你的Presenter依赖PreviewsUseCase和PreviewView接口,完整测试代码如下:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.BDDMockito.then

class PreviewPresenterTest {

    // Mock测试依赖
    @Mock
    lateinit var mockView: PreviewView

    @Mock
    lateinit var mockPreviewsUseCase: PreviewsUseCase

    private lateinit var presenter: PreviewPresenter

    // 用测试Dispatcher替代UI线程Dispatcher
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before
    fun setup() {
        // 初始化Mock实例
        MockitoAnnotations.openMocks(this)
        // 替换Main Dispatcher为测试用Dispatcher,确保协程同步执行
        Dispatchers.setMain(testDispatcher)
        // 注入依赖创建Presenter
        presenter = PreviewPresenter(mockView, mockPreviewsUseCase)
    }

    @After
    fun teardown() {
        // 重置Main Dispatcher,避免影响其他测试
        Dispatchers.resetMain()
    }

    @Test
    fun `loadPreviews should fetch items from use case and show them in view`() = runTest {
        // Given:准备测试数据并Stub UseCase的挂起方法
        val testPreviews = listOf(PreviewItem("id1", "Test Preview 1"), PreviewItem("id2", "Test Preview 2"))
        givenSuspended { mockPreviewsUseCase.getPreviews() } willReturnSuspended testPreviews

        // When:调用Presenter的目标方法
        presenter.loadPreviews()

        // Then:验证交互逻辑是否符合预期
        then(mockPreviewsUseCase).should().getPreviews()
        then(mockView).should().showPreviews(testPreviews)
    }
}

// 假设的依赖接口定义
interface PreviewView {
    fun showPreviews(items: List<PreviewItem>)
}

interface PreviewsUseCase {
    suspend fun getPreviews(): List<PreviewItem>
}

data class PreviewItem(val id: String, val title: String)

关键细节说明

  • 协程测试处理:用UnconfinedTestDispatcher替换Dispatchers.Main,确保Presenter里的launch(UI)协程在测试中同步执行,避免异步时序问题;runTest是kotx-coroutines-test提供的测试协程作用域,自动管理协程生命周期。
  • BDD风格优势:givenSuspended封装了runBlocking,让我们可以在BDD的given块中直接调用挂起函数,不用重复嵌套协程代码,保持测试代码的可读性。
  • 异常场景扩展:如果需要测试UseCase抛出异常的情况,可以扩展测试用例:
@Test
fun `loadPreviews should handle error from use case`() = runTest {
    // Given:Stub UseCase抛出异常
    val testError = RuntimeException("Failed to load previews")
    givenSuspended { mockPreviewsUseCase.getPreviews() } willThrow testError

    // When:调用方法
    presenter.loadPreviews()

    // Then:验证View是否正确处理异常(假设View有showError方法)
    then(mockView).should().showError(testError.message)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:16:28