技术问询:语句执行顺序与JUnit测试等待异步任务方案
嘿,我来帮你拆解这两个问题:
先梳理你的代码逻辑:当调用myViewModel.doSomething()时,主线程会立即执行创建Task的同步代码,然后直接继续往下走(比如你测试里写的Thread.sleep...),完全不会等待Task的后台逻辑完成。
而Task的执行流程是分阶段的:
inBackground()会在后台线程异步启动,里面的model.getData()(最长4000ms)会先跑完- 等
inBackground()执行完毕后,onMainThread()会被调度回主线程执行,这时才会触发里面的displayData()
所以整体的执行优先级是:
- 主线程中
doSomething()里的同步代码(创建Task的语句) - 后台线程的
inBackground()(包含model.getData()) - 主线程的
onMainThread()里的displayData()
另外要注意:你代码里的displayData()是MyViewModel的成员方法,但它不会在doSomething()结束后自动执行,只有当onMainThread()主动调用它时才会触发。
用Thread.sleep()是最不可靠的做法——要么睡太久浪费测试时间,要么睡不够导致测试失败。这里给你几个更靠谱的方案:
方案1:用CountDownLatch(最直接的Java并发工具)
这是处理异步等待的经典方式,步骤清晰且易实现:
- 在测试里创建一个
CountDownLatch,计数设为1(对应我们要等1个异步事件完成) - 用Mockito监听
displayData()方法,当它被调用时让latch计数减1 - 调用
doSomething()后,让测试线程等待latch完成
修改后的测试代码示例:
@UiThreadTest @Test public void someTest() throws Exception { CountDownLatch latch = new CountDownLatch(1); MyViewModel myViewModel = Mockito.spy(new MyViewModel()); // 监听displayData(),调用时触发latch计数递减 Mockito.doAnswer(invocation -> { latch.countDown(); return null; }).when(myViewModel).displayData(); myViewModel.doSomething(); // 最多等5秒(比异步任务最长4000ms多1秒缓冲),超时直接标记测试失败 if (!latch.await(5, TimeUnit.SECONDS)) { fail("异步任务超时未完成"); } // 这里可以加断言验证,比如确认displayData()确实被调用过 Mockito.verify(myViewModel, Mockito.times(1)).displayData(); }
方案2:改造ViewModel,让异步状态可观察
如果你的项目用了Android Architecture Components(比如LiveData),可以把异步任务的完成状态用LiveData暴露出来,测试里通过观察LiveData来等待:
先改造ViewModel:
class MyViewModel{ private MutableLiveData<Boolean> taskCompleted = new MutableLiveData<>(); public LiveData<Boolean> getTaskCompleted() { return taskCompleted; } public void doSomething(){ new Task(){ void inBackground(){ model.getData(); void onMainThread(){ displayData(); taskCompleted.postValue(true); // 任务完成时发送通知 } } } } void displayData(){ } }
然后测试代码可以这样写:
@UiThreadTest @Test public void someTest() throws Exception { MyViewModel myViewModel = new MyViewModel(); CountDownLatch latch = new CountDownLatch(1); myViewModel.getTaskCompleted().observeForever(completed -> { if (completed) { latch.countDown(); } }); myViewModel.doSomething(); latch.await(5, TimeUnit.SECONDS); // 断言任务已完成 assertTrue(myViewModel.getTaskCompleted().getValue()); }
方案3:用Espresso的IdlingResource(适合Android UI测试)
如果这是Android UI测试场景,Espresso的IdlingResource可以自动等待异步任务完成,不用手动写latch。核心是把你的Task和IdlingResource绑定:
先定义一个IdlingResource:
public class TaskIdlingResource implements IdlingResource { private ResourceCallback callback; private boolean isIdle = false; @Override public String getName() { return "TaskIdlingResource"; } @Override public boolean isIdleNow() { return isIdle; } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; } public void setIdle(boolean idle) { isIdle = idle; if (idle && callback != null) { callback.onTransitionToIdle(); } } }
然后在ViewModel里注入这个资源,任务开始时设为setIdle(false),完成时设为setIdle(true),测试里注册该资源后,Espresso会自动等待任务完成再执行后续断言。
内容的提问来源于stack exchange,提问作者Dishonered

