【问题标题】:Unit test with Coroutines and Retrofit使用 Coroutines 和 Retrofit 进行单元测试
【发布时间】:2019-01-11 09:42:27
【问题描述】:

我使用协程和改造创建了一个应用程序,它运行良好。当我尝试为 Presenter 创建 UT 时,问题就来了。以下是我制作演示者的方式:

class MainPresenter : ViewModel() {

    private val compositeDisposable = CompositeDisposable()
    private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
    private lateinit var listener: ActivityStatesListener

    fun setActivityListener(listener: ActivityStatesListener) {
        this.listener = listener
    }

    fun getHeroesFromRepository(page: Int) {
        GlobalScope.launch(Dispatchers.Main) {
            try {
                val response = heroesRepository.getHeroes(page)
                listener.onHeroesReady(response.data.results)
            } catch (e: HttpException) {
                listener.onError(e.message())
            } catch (e: Throwable) {
                listener.onError(e.message)
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        compositeDisposable.dispose()
    }
}

我开始为它制作 UT,我做了一个小测试,但给了我以下错误:java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize

class HeroesDataSourceTest {

    val heroesRepository: HeroesRepository = mock(HeroesRepository::class.java)
    @Mock
    lateinit var activityListener: ActivityStatesListener

    val hero = Heroes.Hero(1, "superman", "holasuperman", 1, null, null)
    val results = Arrays.asList(hero)
    val data = Heroes.Data(results)
    val dataResult = Heroes.DataResult(data)

    private val mainPresenter = MainPresenter()

    @Before
    fun initTest() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun testLoadInitialSuccess() = runBlocking(Dispatchers.Main) {
        `when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
        mainPresenter.getHeroesFromRepository(0)
        verify(activityListener).onHeroesReady(dataResult.data.results)
    }
}

很明显 Dispatcher.Main 正在给出问题,但我不知道如何解决它。

编辑

使用的存储库如下:

class HeroesRepository {

    val privateKey = "5009bb73066f50f127907511e70f691cd3f2bb2c"
    val publicKey = "51ef4d355f513641b490a80d32503852"
    val apiDataSource = DataModule.create()
    val pageSize = 20

    suspend fun getHeroes(page: Int): Heroes.DataResult {
        val now = Date().time.toString()
        val hash = generateHash(now + privateKey + publicKey)
        val offset: Int = page * pageSize
        return apiDataSource.getHeroes(now, publicKey, hash, offset, pageSize).await()
    }

    fun generateHash(variable: String): String {
        val md = MessageDigest.getInstance("MD5")
        val digested = md.digest(variable.toByteArray())
        return digested.joinToString("") {
            String.format("%02x", it)
        }
    }
}

【问题讨论】:

    标签: unit-testing kotlin coroutine kotlinx.coroutines


    【解决方案1】:

    我假设heroesRepository.getHeroes(page)被标记为suspend,所以它会暂停协程而不阻塞主线程。 尝试遵循下一种方法:

    // add `CoroutineContext` to the constructor to be replaceable from the tests
    class MainPresenter(private val uiContext: CoroutineContext = Dispatchers.Main) 
        : ViewModel(), CoroutineScope {
    
        private var job: Job = Job()
        override val coroutineContext: CoroutineContext
            get() = uiContext + job
    
        fun getHeroesFromRepository(page: Int) {
            // use local scope to launch a coroutine
            launch {
                try {
                    val response = heroesRepository.getHeroes(page)
                    listener.onHeroesReady(response.data.results)
                } catch (e: HttpException) {
                    listener.onError(e.message())
                } catch (e: Throwable) {
                    listener.onError(e.message)
                }
            }
        }
    
        override fun onCleared() {
            super.onCleared()
            job.cancel()
        }
    
        // ...
    }
    

    在测试类中将uiContext 替换为另一个CoroutineContext

    class HeroesDataSourceTest {
        // ... initializations
    
        @Test
        fun testLoadInitialSuccess() = runBlocking {
            `when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
            mainPresenter = MainPresenter(Dispatchers.Unconfined).apply {
                getHeroesFromRepository(0)
            }
    
            // ... your tests here
        }
    }
    

    【讨论】:

    • 这解决了我遇到的问题,但现在的问题是测试继续进行,而无需等待 getHeroesFromRepository 函数执行,因此我无法测试它是否达到了应有的效果。
    • getHeroes(page) 函数标记为suspend
    • 是的。我将使用存储库进行编辑,以便您检查它
    • 一切都应该工作。您正在模拟 heroesRepository.getHeroes(0) 调用,因此您的 getHeroesFromRepository 函数将足够快地返回。请再次检查。
    • 我检查了几次并调试它。当我调用getHeroesFromRepository´ it enters and after the line val response = herosRepository.getHeroes(page)` 时,它直接转到verify(activityListener).onHeroesReady(dataResult.data.results)。我还在两个 catch 中都设置了断点,但它没有继续。
    猜你喜欢
    • 2021-08-09
    • 1970-01-01
    • 1970-01-01
    • 2015-11-29
    • 1970-01-01
    • 1970-01-01
    • 2019-12-17
    • 2015-03-06
    • 2011-02-20
    相关资源
    最近更新 更多