Android, Unit test 대상 로직에서 Handler 사용 시 멈추는 문제 해결 by 오리대마왕

이전 포스트에서 async task의 실행은 countdown latch와 runOnUiThread 를 이용해 성공했다.
그러나 여러 작업을 실행할 경우, unit test가 그냥 멈춰버리는 증상이 발생했다.

한참 디버거와 씨름을 하니, 이 문제는 Handler가 범인이었다. handler.post() 에 넣은 내용이 실행되지 않아 countdown latch.countdown() 이 실행되지 않으니 계속 대기상태에 빠질 수 밖에. 

그럼 대체 왜 handler.post() 의 내용이 실행되지 않았는가? .. 여기까진 모르겠다. 하여간 이 문제는 Handler.post() 를 아주 단순히 오버라이딩 해 버려서 해결해다. (참고: http://stackoverflow.com/questions/4914946/unit-testing-handler )

Handler.post() 는 final이기 때문에 직접 override할 순 없어, 내부에서 사용하는 sendMessageAtTime 을 override했다.
이렇게 만든, 테스트를 위한 엉터리 Handler는 다음과 같다.


class TestHandler extends Handler {
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
msg.getCallback().run();
return true;
}
}


이렇게 TestHandler 와 지난번에 얘기한 countDownLatcher를 함께 사용한 일종의 테스트 framework은 대략 다음과 같이 구현할 수 있다.


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import junit.framework.TestCase;
import android.os.Handler;
import android.os.Message;
import android.test.InstrumentationTestCase;
public class ChainedAsycnTaskExecutor {
// 실제 테스트케이스 클래스
InstrumentationTestCase tc;
// async 작업이 끝날때 까지 대기할 용도의 latch
CountDownLatch latch;
public ChainedAsycnTaskExecutor(InstrumentationTestCase tc) {
this.tc = tc;
this.latch = new CountDownLatch(1);
}
/-*
* 테스트 실행
* @throws Throwable
*-
public void runTasks() throws Throwable {
final Handler h = new TestHandler();
Runnable test = new Runnable() {
@Override
public void run() {
init(h);
runNextTask();
}
};
// UI thread에서 실행
tc.runTestOnUiThread(test);
// 테스트 항목들이 종료될 때 까지 대기
latch.await();
}
// 개별 task 실행
private void runNextTask() {
if (tests.isEmpty()) {
// 모든 task 종료했으면 끝냄
latch.countDown();
return;
}
final AsyncTest t = tests.remove(0); // 다음 task 가져옴
t.exec(new TaskCallback(t.cb));
}
void init(final Handler h) {
// 이런 저런 초기화 작업
}
void addTest(AsyncTest t) {
tests.add(t);
}
List<AsyncTest> tests = new ArrayList<ChainedAsycnTaskExecutor.AsyncTest>();
static abstract class AsyncTest {
ApiCallback cb;
AsyncTest(ApiCallback cb) {
this.cb = cb;
}
public abstract void exec(TaskCallback testApiCallback);
}
class TestHandler extends Handler {
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
msg.getCallback().run();
return true;
}
}
class TaskCallback implements ApiCallback { // 원래 콜백을 가로채서 countdown latch 처리를 대신 할 콜백
ApiCallback originalCb;
TaskCallback(ApiCallback cb) {
originalCb = cb;
}
@Override
public void onError(Exception e) {
System.out.println("TaskCallback.on error!");
e.printStackTrace();
latch.countDown();
TestCase.fail();
}
@Override
public void onSuccess(Object result) {
System.out.println("TaskCallback.on success!");
originalCb.onSuccess(result);
try {
runNextTask();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
static interface ApiCallback { // 원래 실행될 콜백
void onError(Exception e);
void onSuccess(Object result);
}
}


그리고 이를 사용한 TestCase는 다음과 같다.


import android.os.AsyncTask;
import android.test.InstrumentationTestCase;
import ChainedAsycnTaskExecutor.ApiCallback;
import ChainedAsycnTaskExecutor.AsyncTest;
import ChainedAsycnTaskExecutor.TaskCallback;
public class ChainedAsycnTaskExecutorTest extends InstrumentationTestCase {
String testResult=null;
final String expected="well done!";
public void testTF1() throws Throwable {
ChainedAsycnTaskExecutor tf = new ChainedAsycnTaskExecutor(this);
ChainedAsycnTaskExecutor.ApiCallback cb = new ChainedAsycnTaskExecutor.ApiCallback() {
@Override
public void onSuccess(Object value) {
testResult = (String)value;
}
@Override
public void onError(Exception e) {
System.err.println("err!");
}
};
tf.addTest(new AsyncTest(cb) {
@Override
public void exec(TaskCallback testApiCallback) {
new MyAsyncTask(testApiCallback).execute(null);
}
});
tf.runTasks();
assertEquals(expected, testResult);
}
class MyAsyncTask extends AsyncTask {
ApiCallback cb;
MyAsyncTask(ApiCallback cb) {
super();
this.cb = cb;
}
@Override
protected Object doInBackground(Object... params) {
return expected;
}
@Override
protected void onPostExecute(Object result) {
cb.onSuccess(result);
}
@Override
protected void onCancelled() {
cb.onError(new RuntimeException());
}
}
}