如何单元测试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
相关产品推荐
相关产品推荐

