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

Android ViewModel协程:为Dispatcher.IO写单元测试遇执行提前结束问题

问题:单元测试提前结束,无法等待网络请求完成并观察LiveData结果

核心问题:编写单元测试调用ViewModel的网络请求方法后,测试直接通过,DataSource中response.isSuccessful代码行从未执行,测试在异步任务完成前就结束了,无法验证LiveData的结果。


单元测试类

@RunWith(AndroidJUnit4::class)
class TestViewModel {

    private lateinit var mockWebServer: MockWebServer
    private val client = OkHttpClient.Builder()
        .connectTimeout(1, TimeUnit.SECONDS)
        .readTimeout(1, TimeUnit.SECONDS)
        .writeTimeout(1, TimeUnit.SECONDS)
        .build()

    private lateinit var api: TPSCallService

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun setUp()  {
        mockWebServer = MockWebServer()
        api = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(MyAPI::class.java)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun testing() = runTest {
        val mockResponse = MockResponse()
        val inputStream = javaClass.classLoader?.getResourceAsStream("personalDetailsAPI/success.json")
        val source = inputStream?.let { inputStream.source().buffer() }
        mockResponse.setResponseCode(200).setBody(source!!.readString(StandardCharsets.UTF_8))
        mockWebServer.enqueue(mockResponse)

        val dataSource = MyDataSource(api)
        val vm = MainViewModel(dataSource)
        vm.getMyList()
        vm.dataList.observeForever() {
            Assert.assertEquals("",3, it.size)
        }
    }
}

ViewModel类

class MainViewModel @Inject constructor(): ViewModel() {

    constructor(myDataSource: MyDataSource): this() {
        this.storeDataSource = storeDataSource
    }

    companion object {
        private const val TAG = "MainViewModel_TAG"

        val job = SupervisorJob()
        var dispatcher = CoroutineScope(Dispatchers.IO + job)
    }

    private var myDataSource: MyDataSource = MyDataSource()

    private val _dataList = MutableLiveData<List<DataResponse>>()
    public val dataList: LiveData<List<DataResponse>>
        get() = _dataList

    public fun getMyList() {
        dispatcher.launch {
            myDataSource.getStoreList(Constants.DEFAULT_LATITUDE, Constants.DEFAULT_LONGITUDE) { dataLists, code ->
                _dataList.postValue(dataLists.getOrNull())// 从未执行到这里!
            }
        }
    }
}

class MainViewModelFactory() :
    ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return modelClass.getConstructor()
            .newInstance()
    }
}

class MainViewModelFactoryTwo(val dataSource: MyDataSource) :
    ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(MyDataSource::class.java)
            .newInstance(dataSource)
    }
}

DataSource类

class MyDataSource {

    private lateinit var myAPI: MyAPI

    constructor() {
        myAPI = NetworkModule().provideMyAPI() // dagger提供Retrofit实例的方法
    }

    constructor(myAPI: MyAPI) {
        this.myAPI = myAPI
    }

    public suspend fun getStoreList(lat: Double, lng: Double, completion: (Result<List<StoreResponse>>, Int) -> Unit) {
        val response = myAPI.getMyList(lat, lng).awaitResponse()

        if (response.isSuccessful) { // 断点到不了这里!测试在执行前就结束了
            response.body()?.let {
                completion(Result.success(it), response.code())
            } ?: run {
                completion(Result.failure(Throwable()), response.code())
            }
        } else {
            completion(Result.failure(Throwable()), response.code())
        }
    }
}

解决方案

1. 改造ViewModel,支持Dispatcher注入

ViewModel中硬编码的Dispatchers.IO导致测试无法控制协程执行时机,需将Dispatcher设计为可注入参数:

class MainViewModel @Inject constructor(
    private val myDataSource: MyDataSource,
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
): ViewModel() {
    // 移除companion中硬编码的dispatcher,改用ViewModel级别的协程作用域
    private val viewModelScope = CoroutineScope(SupervisorJob() + dispatcher)

    private val _dataList = MutableLiveData<List<DataResponse>>()
    public val dataList: LiveData<List<DataResponse>>
        get() = _dataList

    public fun getMyList() {
        viewModelScope.launch {
            myDataSource.getStoreList(Constants.DEFAULT_LATITUDE, Constants.DEFAULT_LONGITUDE) { dataLists, code ->
                _dataList.postValue(dataLists.getOrNull())
            }
        }
    }

    // 保留测试用构造函数
    constructor(myDataSource: MyDataSource) : this(myDataSource, Dispatchers.IO)
}

2. 用测试Dispatcher控制协程同步执行

在测试中使用UnconfinedTestDispatcher注入ViewModel,确保协程在测试线程同步执行,避免异步逃逸:

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testing() = runTest {
    val mockResponse = MockResponse()
    val inputStream = javaClass.classLoader?.getResourceAsStream("personalDetailsAPI/success.json")
    val source = inputStream?.let { inputStream.source().buffer() }
    mockResponse.setResponseCode(200).setBody(source!!.readString(StandardCharsets.UTF_8))
    mockWebServer.enqueue(mockResponse)

    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    val dataSource = MyDataSource(api)
    val vm = MainViewModel(dataSource, testDispatcher)
    
    vm.getMyList()
    
    // 直接获取LiveData更新后的值
    val result = vm.dataList.getOrAwaitValue()
    Assert.assertEquals(3, result.size)
}

3. 添加LiveData测试扩展函数

如果需要等待LiveData异步更新,可添加以下扩展函数确保测试等待结果:

@OptIn(ExperimentalCoroutinesApi::class)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }
    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

4. 避免无等待的observeForever断言

原测试中observeForever内的断言可能永远不会执行,因为测试线程已提前结束。必须通过主动等待或同步协程执行的方式,确保LiveData更新后再做断言。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 19:15:03