android 单元测试使用处理程序的回调

tzdcorbm  于 2023-06-04  发布在  Android
关注(0)|答案(1)|浏览(199)

bounty将在6天内到期。回答此问题可获得+150声望奖励。user1506104希望引起更多关注这个问题。

我创建了这个短类,它在执行某个进程后向侦听器提供响应。为了使其成为reproducible example,我修改了我的 handleMessage(Message) 函数,使其休眠5秒,并在完成时返回一个随机字符串,而不是实际连接到远程主机。

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 {
                    // Mimic long-running service execution
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                notifyOutboxHandlers(COMMAND_RESPONSE, 0, 0, "<SUCCESS/>");
            }
        };
        thread.start();
    }
}

这门课起作用了。我只需要编写一个单元测试,并确保可以从服务触发侦听器(类型为Handler)。我的视图(类型为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();
    }
}

最后,我的问题是,如何对这个回调函数进行单元测试?如何确保视图层中的处理程序被正确触发。具体来说,我想测试/Assert来自服务的回复是<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 );
    }
}

任何帮助将不胜感激。谢谢!

2nc8po8w

2nc8po8w1#

测试异步代码可能很棘手,因为测试运行器并不等待异步任务完成。在本例中,您尝试测试由异步操作调用的回调。
一种方法是使用CountDownLatch,这是Java中的并发工具,允许一个线程等待,直到另一个线程满足特定条件。(另见Eugen Baeldung的“Guide to CountDownLatch in Java”)
例如,您可以使用以下命令修改测试类:

import android.os.Handler;
import android.os.Message;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

    private MyService mMyService;
    private Handler mHandler;

    @Mock
    private Handler.Callback mHandlerCallback;

    @Captor
    private ArgumentCaptor<Message> messageCaptor;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mMyService = new MyService();
        when(mHandlerCallback.handleMessage(any(Message.class)))
                .thenReturn(true);
    }

    @Test
    public void testCallback() throws InterruptedException {
        // Create a CountDownLatch with a count of 1
        CountDownLatch latch = new CountDownLatch(1);

        // Override the handleMessage method to count down the latch when the message is received
        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                messageCaptor.setValue(msg);
                latch.countDown();
                return true;
            }
        });

        mMyService.registerListener(mHandler);

        // Send a message to the service
        Message test = new Message();
        test.what = COMMAND_REQUEST;
        test.arg1 = 1;
        test.arg2 = 2;
        test.obj = "test";
        mMyService.inform(test);

        // Wait for the latch to count down (up to 10 seconds)
        boolean received = latch.await(10, TimeUnit.SECONDS);

        // Assert that the message was received
        assertTrue(received);

        // Assert that the received message is what you expected
        assertEquals("<SUCCESS/>", messageCaptor.getValue().obj);
    }
}

CountDownLatch用于暂停测试方法的执行,直到调用回调。锁存器在处理程序的handleMessage方法中倒计时,该方法是在消息发送到处理程序时调用的方法。如果在10秒内调用回调,则测试通过。
ArgumentCaptor用于捕获发送到回调的Message对象,以便您可以Assert它是您所期望的。
请确保根据您的服务中操作所需的时间相应地调整latch.await(10, TimeUnit.SECONDS)中的时间。如果操作花费的时间超过提供的超时时间,则测试将失败。
与原始代码的区别:
1.**CountDownLatch:**在更新后的代码中,创建了一个计数为1的CountDownLatch。这个锁存器的作用是暂停测试方法的执行,直到调用回调。当调用回调函数时,锁存器将倒计时。如果锁存器在指定的超时(本例中为10秒)内倒计时,则测试通过。如果在超时时间内没有调用回调,则测试失败。这是用于测试异步代码的常用技术。
1.**创建句柄:**原代码中,Handler是用ArgumentCaptor捕获的Callback创建的。这样做是为了捕获Callback对象,以便稍后进行验证。然而,在更新的代码中,Handler是用Callback创建的,当CountDownLatch收到消息时,它会倒计时。这样做是为了发出已调用回调的信号。
1.**Assert:**在原始代码中,测试AssertCallbackHandler.Callback的示例。但是,在更新的代码中,测试Assert回调接收到的Message是预期的。这是一个更有意义的测试,因为它验证服务是否按预期运行,而不仅仅是它是否调用回调。
1.**消息捕获:**在更新后的代码中,使用ArgumentCaptor捕获发送给回调的Message对象。这样做是为了让您可以Assert消息是您所期望的。在原始代码中,没有这样的捕获。
1.**异常处理:**更新后的测试方法声明抛出InterruptedException。这是必需的,因为如果当前线程在等待闩锁倒计时时中断,latch.await()可能会抛出此异常。

相关问题