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

Android MVP架构下Presenter测试方法咨询(Mockito/Mockk、RxJava)

我来帮你一步步梳理如何测试这个Presenter,还有你提到的RxJava调度器的问题,以及测试范围的疑问~

测试MyPresenterImpl的实践指南

一、先明确测试目标

Presenter作为View和UseCase的中间层,我们的测试核心是验证:

  • Presenter是否正确触发了View的对应方法(比如加载成功显示结果、失败显示错误)
  • Presenter是否正确与UseCase交互(调用了execute方法)
  • 订阅管理逻辑是否正常(比如取消订阅、生命周期处理)

二、哪些方法需要测试?

不是所有方法都要写测试用例,优先覆盖有业务逻辑或状态变化的方法:

  • ✅ loadResults():核心业务方法,必须测成功、失败两种场景
  • ✅ rxJavaUnsuscribe():订阅取消逻辑,要测已释放和未释放两种情况
  • ⚙️ attachView()/detachView():如果只是简单的赋值+调用showProgressBar,可以选测(主要验证View方法是否被触发)

三、为什么要配置RxJava的Schedulers为Trampoline?

在Android环境中,RxJava默认用Schedulers.io()(异步IO线程)和AndroidSchedulers.mainThread()(主线程)执行任务,这会导致测试用例在异步任务完成前就结束,断言直接失败。

Schedulers.trampoline()的作用是让所有RxJava任务在当前线程同步执行,相当于把异步操作变成同步,这样测试就能按顺序跑完所有逻辑,不需要等待异步回调。

配置示例(JUnit 4)

在测试类的初始化和清理方法中全局替换调度器:

@Before
fun setUp() {
    // 替换所有RxJava调度器为同步线程
    RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
    RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
    RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
    // 替换Android主线程调度器
    RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
}

@After
fun tearDown() {
    // 测试结束后重置调度器,避免影响其他测试
    RxJavaPlugins.reset()
    RxAndroidPlugins.reset()
}

四、具体测试用例(Mockito + JUnit 4)

先准备好测试依赖:Mock核心类、被测试的Presenter,然后分场景写用例。

1. 测试attachView方法

验证绑定View时是否正确调用了进度条显示:

class MyPresenterImplTest {
    @Mock
    private lateinit var myUseCase: MyUseCase
    @Mock
    private lateinit var view: MyContract.View
    
    private lateinit var presenter: MyPresenterImpl

    @Before
    fun setUp() {
        // 初始化Mockito
        MockitoAnnotations.openMocks(this)
        // 配置RxJava同步调度器
        RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
        RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
        // 创建Presenter实例
        presenter = MyPresenterImpl(myUseCase)
    }

    @After
    fun tearDown() {
        RxJavaPlugins.reset()
        RxAndroidPlugins.reset()
    }

    @Test
    fun `attach view should trigger progress bar display`() {
        // 执行绑定操作
        presenter.attachView(view)
        
        // 断言View的showProgressBar(true)被调用
        verify(view).showProgressBar(true)
        // 验证View实例被正确持有
        assertNotNull(presenter.mView)
    }
}

2. 测试loadResults成功场景

验证UseCase返回成功结果时,Presenter正确更新View:

@Test
fun `load results should show data when use case succeeds`() {
    // 准备测试数据
    val testData = listOf("Result 1", "Result 2")
    // Mock UseCase的execute方法,返回成功的Observable
    `when`(myUseCase.execute()).thenReturn(Observable.just(testData))
    
    // 先绑定View
    presenter.attachView(view)
    // 执行加载逻辑
    presenter.loadResults()
    
    // 按顺序验证方法调用(确保进度条先隐藏,再显示结果)
    inOrder(view).apply {
        verify(view).showProgressBar(true) // attach时的调用
        verify(view).showProgressBar(false)
        verify(view).showResults(testData)
    }
}

3. 测试loadResults失败场景

验证UseCase抛出异常时,Presenter正确显示错误:

@Test
fun `load results should show error when use case fails`() {
    // 准备测试异常
    val testError = RuntimeException("Network error")
    // Mock UseCase返回错误的Observable
    `when`(myUseCase.execute()).thenReturn(Observable.error(testError))
    
    presenter.attachView(view)
    presenter.loadResults()
    
    inOrder(view).apply {
        verify(view).showProgressBar(true)
        verify(view).showProgressBar(false)
        verify(view).showError(testError.localizedMessage)
    }
}

4. 测试rxJavaUnsuscribe逻辑

验证未释放的订阅会被正确取消,已释放的不会重复取消:

@Test
fun `unsubscribe should dispose active disposable`() {
    // Mock一个未释放的Disposable
    val mockDisposable = mock(Disposable::class.java)
    `when`(mockDisposable.isDisposed).thenReturn(false)
    // 给Presenter设置这个Disposable(如果是private字段,可以用反射或者改成internal)
    presenter.disposable = mockDisposable
    
    presenter.rxJavaUnsuscribe()
    
    // 验证dispose()被调用
    verify(mockDisposable).dispose()
}

@Test
fun `unsubscribe should not dispose already disposed disposable`() {
    val mockDisposable = mock(Disposable::class.java)
    `when`(mockDisposable.isDisposed).thenReturn(true)
    presenter.disposable = mockDisposable
    
    presenter.rxJavaUnsuscribe()
    
    // 验证dispose()从未被调用
    verify(mockDisposable, never()).dispose()
}

五、关于Mockk的小提示

你说Mockk没能成功运行,大概率是依赖配置或语法问题。比如Mockk需要引入io.mockk:mockk:x.y.z,而且Kotlin版本要兼容。如果换成Mockk,核心逻辑和上面一样,只是语法更简洁:

  • 用mockk<MyUseCase>()创建Mock实例
  • 用every { myUseCase.execute() } returns Observable.just(testData)设置返回值
  • 用verify { view.showResults(testData) }验证方法调用

最后总结

  • 优先覆盖核心业务逻辑和状态变化的方法,不用为简单的get/set写测试
  • 必须配置RxJava同步调度器,否则异步操作会导致测试失效
  • 用Mock工具(Mockito/Mockk)隔离View和UseCase,只关注Presenter的逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:27:49