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

如何单元测试基于Handler的服务回调,验证返回<SUCCESS/>

问题描述

我创建了一个简易类MyService,它在执行特定流程后向监听器返回响应。为了提供可复现示例,我修改了*handleMessage(Message)*方法,使其睡眠5秒后返回固定字符串,而非实际连接远程主机。

MyService代码

public class MyService {
    private static final String TAG = MyService.class.getSimpleName();

    private final HandlerThread mInboxHandlerThread;
    private final Handler mInboxHandler;
    private final List<Handler> mListeners = new ArrayList<>();

    public MyService() {
        mInboxHandlerThread = new HandlerThread("Incoming data handler");
        mInboxHandlerThread.start();

        mInboxHandler = new Handler(mInboxHandlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                MyService.this.handleMessage(msg);
            }
        };
    }

    public final void inform(Message msg) {
        if (mInboxHandler != null) {
            mInboxHandler.sendMessage(msg);
        } else {
            Log.w(TAG, "mInboxHandler is null");
        }
    }

    public final void registerListener(Handler listener) {
        mListeners.add(listener);
    }

    public final void unregisterListener(Handler listener) {
        mListeners.remove(listener);
    }

    final void notifyOutboxHandlers(int what, int arg1, int arg2, Object obj) {
        if (mListeners.isEmpty()) {
            Log.w(TAG, String.format("No listener to handle outgoing message (%d)", what));
        } else {
            for (Handler handler : mListeners) {
                Message msg = Message.obtain(handler, what, arg1, arg2, obj);
                msg.sendToTarget();
            }
        }
    }

    private void handleMessage(Message msg) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    // 模拟耗时服务执行
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                notifyOutboxHandlers(COMMAND_RESPONSE, 0, 0, "<SUCCESS/>");
            }
        };
        thread.start();
    }
}

视图层Activity代码

public class MainActivity extends AppCompatActivity implements Handler.Callback {

    private final static String TAG = MainActivity.class.getSimpleName();
    private MyService mMyService = null;
    private TextView mMessagesView = null;
    private Handler mHandler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cc);
        mMessagesView = findViewById(R.id.messages);

        mMyService = new MyService();
        mHandler = new Handler(this);
        mMyService.registerListener(mHandler);

        Message test = new Message();
        test.what = COMMAND_REQUEST;
        test.arg1 = 1;
        test.arg2 = 2;
        test.obj = "test";
        mMyService.inform(test);
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        String messages = mMessagesView.getText().toString();
        mMessagesView.setText(messages + "\nTransactionManager: " + msg.obj);

        switch (msg.what) {
            case COMMAND_RESPONSE:
                Log.d(TAG, "got response from controller");
                return true;
        }
        return false;
    }

    @Override
    protected void onDestroy() {
        mMyService.unregisterListener(mHandler);
        super.onDestroy();
    }
}

我的问题

如何对该回调函数进行单元测试?如何确保视图层的Handler被正确触发?具体来说,我想要测试/断言服务返回的内容为<SUCCESS/>。

我参考其他帖子编写了测试脚本,但无法正常运行:

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

    private MyService mMyService;
    private Handler mHandler;

    @Mock
    private Handler.Callback mHandlerCallback;

    @Captor
    private ArgumentCaptor<Handler.Callback> callbackCaptor;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        mMyService = new MyService();
        when(mHandlerCallback.handleMessage(any(Message.class)))
                .thenReturn(true);
    }

    @Test
    public void testCallback() {
        String message = "test";

        Message test= new Message();
        test.what = COMMAND_REQUEST;
        test.arg1 = 1;
        test.arg2 = 2;
        test.obj = message;

        mHandler = new Handler(callbackCaptor.capture());
        mMyService.registerListener(mHandler);
        mMyService.inform(test);

        assertTrue(callbackCaptor instanceof Handler.Callback );
    }
}

恳请提供帮助,谢谢!


解决方案

你的测试代码存在几个关键问题,以下是修正后的测试方案:

问题分析

  1. 错误使用ArgumentCaptor:应该直接用Mock的Handler.Callback创建Handler,而非捕获构造参数。
  2. 未处理异步等待:MyService包含5秒睡眠和线程执行,测试需要等待回调触发后再做断言。
  3. 断言逻辑无效:callbackCaptor instanceof Handler.Callback没有实际测试意义,需要验证回调的调用参数是否符合预期。

修正后的测试代码

@RunWith(AndroidJUnit4.class)
public class MyServiceCallbackTest {

    private MyService mMyService;
    private Handler mTestHandler;

    @Mock
    private Handler.Callback mMockCallback;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        // 使用主线程Looper创建Handler,确保回调能在测试环境中被处理
        mTestHandler = new Handler(Looper.getMainLooper(), mMockCallback);
        mMyService = new MyService();
        mMyService.registerListener(mTestHandler);
    }

    @Test
    public void testCallbackReturnsSuccess() throws InterruptedException {
        // 构造测试消息
        Message testMsg = Message.obtain();
        testMsg.what = COMMAND_REQUEST;
        testMsg.arg1 = 1;
        testMsg.arg2 = 2;
        testMsg.obj = "test";

        // 触发服务逻辑
        mMyService.inform(testMsg);

        // 等待异步操作完成(设置5.5秒,覆盖模拟的5秒睡眠)
        Thread.sleep(5500);

        // 验证回调被调用,且参数符合预期
        ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
        verify(mMockCallback, times(1)).handleMessage(messageCaptor.capture());

        Message receivedMsg = messageCaptor.getValue();
        assertEquals(COMMAND_RESPONSE, receivedMsg.what);
        assertEquals("<SUCCESS/>", receivedMsg.obj);
    }

    @After
    public void teardown() {
        // 测试后注销监听器,避免影响其他测试
        mMyService.unregisterListener(mTestHandler);
    }
}

额外优化建议

  1. 用CountDownLatch替代固定睡眠:更优雅地等待异步操作完成,避免硬编码等待时间:
@Test
public void testCallbackWithLatch() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(1);

    // 自定义回调,触发后释放latch
    Handler.Callback testCallback = msg -> {
        if (msg.what == COMMAND_RESPONSE) {
            latch.countDown();
        }
        return true;
    };

    Handler testHandler = new Handler(Looper.getMainLooper(), testCallback);
    mMyService.registerListener(testHandler);

    // 触发服务
    Message testMsg = Message.obtain();
    testMsg.what = COMMAND_REQUEST;
    mMyService.inform(testMsg);

    // 最多等待10秒,超时则测试失败
    assertTrue(latch.await(10, TimeUnit.SECONDS));

    // 断言返回内容
    // 可结合ArgumentCaptor验证msg.obj为<SUCCESS/>
}
  1. 提升MyService可测试性:将线程执行逻辑抽象为接口,测试时用同步实现代替异步,彻底避免等待时间:
// 抽象执行器接口
public interface TaskExecutor {
    void execute(Runnable task);
}

// MyService中替换直接创建线程的逻辑
private final TaskExecutor mExecutor;

public MyService(TaskExecutor executor) {
    mExecutor = executor;
    // 其他初始化逻辑...
}

private void handleMessage(Message msg) {
    mExecutor.execute(() -> {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyOutboxHandlers(COMMAND_RESPONSE, 0, 0, "<SUCCESS/>");
    });
}

// 测试时使用同步执行器
@Test
public void testCallbackSync() {
    TaskExecutor syncExecutor = Runnable::run;
    MyService testService = new MyService(syncExecutor);
    // 后续测试逻辑无需等待,直接断言
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:40:08