Let's take as reference this simple interface:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface SampleClass { | |
void doAsync(Listener l); | |
public interface Listener{ | |
void onValueChanged(int i); | |
} | |
} |
The first thing is: how can we test it through an Android unit test class?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TestSample extends AndroidTestCase{ | |
public void testDoAsync(){ | |
SampleClass s = new SampleClass(); | |
Listener l = new ListenerImpl(); | |
s.doAsync(); | |
} | |
private class ListenerImpl implements Listener{ | |
public void onValueChanged(int i){ | |
//how to test this value? | |
} | |
} | |
} |
We can't test the value inside the callback. Infact, an async call runs usually in worker thread. When the thread has done some computation, it calls the callback. The simplest case is when the callback is run in the same worker thread.
But still, we need to find a way to block the main thread, otherwise the test class will simply don't wait before the callback is called! Remember that the method testDoSync() is called in the main thread.
We can use a semaphore, and make the test method wait until the callback is delivered. Then we can easily test the return value. The Semaphore class has some interesting properties: first, we can use it to put a thread in wait; the acquire() method stops the thread until a resource is available. As we have created the Semaphore with 0 resources available, this will cause the next call to acquire() to stop the thread.
But what if there is the unlikely case that the listener, returns before the acquire() call is made? No worries, as the signal() will simply increment the number of resources available and so the acquire() method will not nlock but will return straight away. Yay, this is for sure a better way to handle threads synchronization that the usual pattern wait-notify!
This is the code that covers this case:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TestSample extends AndroidTestCase{ | |
private int mFinalValue; | |
private static final int EXPECTED_VALUE = 10; | |
public void testDoAsync(){ | |
SampleClass s = new SampleClass(); | |
Listener l = new ListenerImpl(); | |
Semaphore semaphore = new Semaphore(0); | |
s.doAsync(); | |
semaphore.acquire(); | |
assertEquals(mFinalValue, EXPECTED_VALUE); | |
} | |
private class ListenerImpl implements Listener{ | |
public void onValueChanged(int i){ | |
mFinalValue = i; | |
semaphore.release(); | |
} | |
} | |
} |
And now, the worst case. Let's imagine that the code run by the worker thread do some stuff, and then it posts the results of its work in the calling thread (for instance through a handler). Then, onValueChanged() will never be called. Why? It is because we needed to stop the main thread. The main thread is a looper thread: normally a thread do some stuff and then it dies. Instead, a thread with a looper processes events by looping continuously. Another thread can post events on it: the events are put in the thread queue and they are processed one at time. If we block the main thread, the event posted by the worker thread will NEVER get processed.
How to handle this case?
We still need to block the main thread. But we need to launch the callback from a secondary thread with the looper.
We will use a facility offered by android: HandlerThread.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TestSample extends AndroidTestCase{ | |
private int mFinalValue; | |
private static final int EXPECTED_VALUE = 10; | |
private final HandlerThread mHandlerThread; | |
private SampleClass mInstance; | |
public void testDoAsync(){ | |
mInstance = new SampleClass(); | |
Listener l = new ListenerImpl(); | |
mHandlerThread = new MyHandler("handlerThread"); | |
Semaphore semaphore = new Semaphore(0); | |
mHandlerThread.start(); | |
semaphore.acquire(); | |
mHandlerThread.quit(); | |
assertEquals(mFinalValue, EXPECTED_VALUE); | |
} | |
private class MyHandler extends HandlerThread{ | |
public void run() { | |
mInstance.doAsync(); | |
} | |
} | |
private class ListenerImpl implements Listener{ | |
public void onValueChanged(int i){ | |
mFinalValue = i; | |
semaphore.release(); | |
} | |
} | |
} |
Finally, remember to quit() the thread handler (that will quit the embedded looper too and will cause the death of that thread).