如何用Mockito模拟Handler.postDelayed?Android单元测试求助
解决Android中Mockito/JUnit测试Handler.postDelayed()的覆盖问题
问题背景
我是Mockito/JUnit测试的初学者,需要在Android Studio中为以下代码完成测试覆盖,重点是模拟Handler及postDelayed()方法:
待覆盖代码(CropSettingViewHandler.java)
Handler h = new Handler(); if(dataHandler.isCropSettingsFirstAccess()) { h.postDelayed(new Runnable() { @Override public void run() { cropSettingsView.getCropTypeInfoButton().performClick(); } }, 100); dataHandler.setCropSettingsFirstAccess(false); }
我的测试代码(CropSettingViewHandlerTest.java)
@Test public void testCropSettingsFirstAccess() { ArgumentCaptor<CategoryButtons.CategoryButtonsEventListener> categoryButtonEventListerCaptor = ArgumentCaptor.forClass(CategoryButtons.CategoryButtonsEventListener.class); when(mockCropSettingsView.getCropSettingsCategoryButton()).thenReturn(mockCategoryButtons); verify(mockCategoryButtons, times(1)).setCategoryButtonsEventListener(categoryButtonEventListerCaptor.capture()); CategoryButtons.CategoryButtonsEventListener categoryButtonEventListener = categoryButtonEventListerCaptor.getValue(); categoryButtonEventListener.onCategoryButtonsAfterExpand(mockCategoryButtons); categoryButtonEventListener.onCategoryButtonsBeforeExpand(mockCategoryButtons); categoryButtonEventListener.onCategoryButtonsAfterCollapse(mockCategoryButtons); categoryButtonEventListener.onCategoryButtonsBeforeCollapse(mockCategoryButtons); when(mockDataHandler.isCropSettingsFirstAccess()).thenReturn(true); Assert.assertEquals(mockDataHandler.isCropSettingsFirstAccess(),true); ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); Runnable runnable = runnableArgumentCaptor.getValue(); runnable.run(); verify(mockCropSettingsView).postDelayed(runnableArgumentCaptor.capture(),100); }
问题分析
你的测试代码存在几个核心问题:
- 原代码直接
new Handler(),无法被Mockito直接模拟,必须通过依赖注入或静态模拟解决。 - 测试逻辑顺序错误:先尝试获取Runnable再执行verify,导致
runnableArgumentCaptor.getValue()返回null。 - 错误验证
mockCropSettingsView.postDelayed(),但原代码中是Handler调用该方法,并非CropSettingsView。
解决方案
方案1:改造原代码(推荐,提升可测试性)
将Handler作为依赖注入到CropSettingViewHandler,避免直接在方法内实例化:
public class CropSettingViewHandler { private final Handler handler; private final DataHandler dataHandler; private final CropSettingsView cropSettingsView; // 通过构造方法注入依赖 public CropSettingViewHandler(Handler handler, DataHandler dataHandler, CropSettingsView cropSettingsView) { this.handler = handler; this.dataHandler = dataHandler; this.cropSettingsView = cropSettingsView; } // 你的业务方法 public void handleCropSettings() { if(dataHandler.isCropSettingsFirstAccess()) { handler.postDelayed(() -> cropSettingsView.getCropTypeInfoButton().performClick(), 100); dataHandler.setCropSettingsFirstAccess(false); } } }
方案2:编写正确的测试代码
基于改造后的代码,编写覆盖测试:
@ExtendWith(MockitoExtension.class) public class CropSettingViewHandlerTest { @Mock private Handler mockHandler; @Mock private DataHandler mockDataHandler; @Mock private CropSettingsView mockCropSettingsView; @Mock private View mockCropTypeInfoButton; // 假设getCropTypeInfoButton返回View private CropSettingViewHandler handlerUnderTest; @BeforeEach void setUp() { handlerUnderTest = new CropSettingViewHandler(mockHandler, mockDataHandler, mockCropSettingsView); when(mockCropSettingsView.getCropTypeInfoButton()).thenReturn(mockCropTypeInfoButton); } @Test void testFirstAccess_TriggersDelayedClickAndSetsFlag() { // 1. 配置首次访问为true when(mockDataHandler.isCropSettingsFirstAccess()).thenReturn(true); // 2. 执行待测试方法 handlerUnderTest.handleCropSettings(); // 3. 验证标记被设置为false verify(mockDataHandler).setCropSettingsFirstAccess(false); // 4. 捕获postDelayed中的Runnable ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockHandler).postDelayed(runnableCaptor.capture(), eq(100)); // 5. 执行Runnable,验证按钮点击被触发 runnableCaptor.getValue().run(); verify(mockCropTypeInfoButton).performClick(); } @Test void testNonFirstAccess_DoesNothing() { // 配置非首次访问 when(mockDataHandler.isCropSettingsFirstAccess()).thenReturn(false); handlerUnderTest.handleCropSettings(); // 验证Handler和DataHandler的方法都未被调用 verify(mockHandler, never()).postDelayed(any(Runnable.class), anyLong()); verify(mockDataHandler, never()).setCropSettingsFirstAccess(false); } }
方案3:无法修改原代码时的静态模拟
如果不能修改原代码,使用Mockito的静态模拟(需Mockito 3.4+)来拦截new Handler():
@ExtendWith(MockitoExtension.class) public class CropSettingViewHandlerTest { @Mock private DataHandler mockDataHandler; @Mock private CropSettingsView mockCropSettingsView; @Mock private View mockCropTypeInfoButton; @Mock private Handler mockHandler; private CropSettingViewHandler handlerUnderTest; @BeforeEach void setUp() { handlerUnderTest = new CropSettingViewHandler(mockDataHandler, mockCropSettingsView); when(mockCropSettingsView.getCropTypeInfoButton()).thenReturn(mockCropTypeInfoButton); } @Test void testFirstAccess_WithStaticMock() { // 模拟Handler构造 try (MockedStatic<Handler> mockedHandler = mockStatic(Handler.class)) { mockedHandler.when(Handler::new).thenReturn(mockHandler); when(mockDataHandler.isCropSettingsFirstAccess()).thenReturn(true); handlerUnderTest.handleCropSettings(); verify(mockDataHandler).setCropSettingsFirstAccess(false); ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mockHandler).postDelayed(runnableCaptor.capture(), eq(100)); runnableCaptor.getValue().run(); verify(mockCropTypeInfoButton).performClick(); } } }
关键注意事项
- 测试顺序:先配置Mock行为,再执行测试方法,最后做验证和参数捕获。
- ArgumentCaptor使用:必须先调用
verify完成捕获,再调用getValue()获取对象,否则会返回null。 - Android环境适配:单元测试使用
org.mockito:mockito-android,Instrumented测试可搭配Robolectric处理Android组件。
内容的提问来源于stack exchange,提问作者Tiago Ribeiro Santos
相关产品推荐
相关产品推荐

