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

如何用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);
}

问题分析

你的测试代码存在几个核心问题:

  1. 原代码直接new Handler(),无法被Mockito直接模拟,必须通过依赖注入或静态模拟解决。
  2. 测试逻辑顺序错误:先尝试获取Runnable再执行verify,导致runnableArgumentCaptor.getValue()返回null。
  3. 错误验证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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 16:40:43