Android Junit 單元測(cè)試、異步測(cè)試方法簡(jiǎn)介及異步測(cè)試框架指南
本文解決的問題
1. 如何使用junit 做Android 單元測(cè)試
2. 如何使用junit 做Android 異步接口單元測(cè)試
3. 使用作者封裝的框架,優(yōu)雅地用junit 做Android 異步接口單元測(cè)試 [doge]
Junit 作為Android Studio 原生支持的測(cè)試框架可以很方便的執(zhí)行單元測(cè)試,并且通過注解 @Test 可以直接標(biāo)記方法為測(cè)試case 然后在子線程中執(zhí)行。 標(biāo)記為@UiThreadTest 時(shí),測(cè)試case 將在 ui線程中執(zhí)行。
但是由于junit 本身的設(shè)計(jì),當(dāng)每個(gè)test方法執(zhí)行結(jié)束時(shí),該方法的運(yùn)行線程會(huì)一并kill掉, 因此對(duì)于異步調(diào)用的方法,子線程會(huì)一并回收,回調(diào)函數(shù)也無法執(zhí)行。
舉個(gè)栗子,以下的測(cè)試case 將無法收到回調(diào)并會(huì)報(bào)錯(cuò)
@Runwith(Junit4.calss)
class Test1{
public static final String TAG="sample test";
@Test
public void test1(){
new YourAsyncJob().run(new YourAsyncTestCallback(){
@Override
public void onFinished(){
Log.i(TAG, "async call back");
}
});
Log.i(TAG, "run async ok");
}
}
解決方法 阻塞 test case
既然測(cè)試線程死掉之后對(duì)應(yīng)子任務(wù)都會(huì)失敗,最直接的方案就是直接阻塞對(duì)應(yīng)
<span id="section1">1 線程鎖</span>
慶幸Java 提供了極其好用的原生api。CountDownLatch 能夠直接阻塞線程,等待完成。 當(dāng)調(diào)用 await()方法時(shí),對(duì)應(yīng)線程會(huì)阻塞至 countdownlatch 的 count 變?yōu)? 時(shí),恢復(fù)運(yùn)行。因此我們得到以下方案
@Runwith(Junit4.calss)
class Test1{
public static final String TAG="sample test";
@Test
public void test1(){
final CountDownLatch mutex = new CountDownLatch(1);
new YourAsyncJob().run(new YourAsyncTestCallback(){
@Override
public void onFinished(){
Log.i(TAG, "async call back");
mutex.countDown();
}
});
Log.i(TAG, "run async ok");
mutex.await();
}
}
跑了一下,似乎可行,log 出來了。
然而在實(shí)際使用中又遇見了新的問題。
2 Looper 阻塞(Handler thread)
做過sdk的同學(xué)可能會(huì)遇見這樣的需求:
業(yè)務(wù)端的同學(xué)主線程(或handler線程)發(fā)起異步請(qǐng)求,執(zhí)行完后(通過handler)回調(diào)至主線程(或handler線程)。
在處理這個(gè)問題是,我們也發(fā)現(xiàn) [方案一]中的回調(diào)函數(shù)事實(shí)上也只能在異步線程中執(zhí)行,而不能切換回發(fā)起線程(測(cè)試線程)中執(zhí)行。這顯然不能滿足我們優(yōu)雅的異步接口的測(cè)試需求。于是我們需要新的方案2 Looper 阻塞 通過looper 阻塞 并且實(shí)現(xiàn)回調(diào)函數(shù)的線程切換。
上述問題的根本就是handler 線程的回調(diào)及切換問題,這個(gè)時(shí)候由于測(cè)試線程是沒有l(wèi)ooper 的,我們需要為它營造一個(gè)這樣的環(huán)境。 同時(shí),既然有l(wèi)ooper 的存在, 那么它的自旋功能也就可以滿足我們對(duì)阻塞的需求,這樣的情況下,我們似乎可以直接拋棄掉之前的CountDownLatch了。
于是我們得到了以下代碼
@Runwith(Junit4.calss)
class Test1{
public static final String TAG="sample test";
@Test
public void test1(){
Looper.prepare();
//final CountDownLatch mutex = new CountDownLatch(1);
new YourAsyncJob().run(new YourAsyncTestCallback(){
@Override
public void onFinished(){
Log.i(TAG, "async call back");
//mutex.countDown();
Looper.myLooper().quitSafely();
}
});
Log.i(TAG, "run async ok");
// mutex.await();
Looper.loop();
}
}
看起來是可以適應(yīng)這樣的過程,于是開始愉快的測(cè)試起來,但是很快,又遇見了新的問題。
3 Handler thread + 封裝
自動(dòng)化測(cè)試好處在于,自動(dòng)的批量地執(zhí)行測(cè)試case。于是在接下來的過程中我們用到了
@RunWith(Parameterized.class)
和@Parameterized.Parameters 注解來執(zhí)行參數(shù)化的批量輸入。
于是新的問題出現(xiàn)了,由于實(shí)際運(yùn)行時(shí)@Test方法運(yùn)行在同一個(gè)子線程,因此多次Looper.prepare() 顯然是不實(shí)際的,(會(huì)有RuntimeException)。
于是最直接解決的辦法是,一開始prepare好么?
事實(shí)上也不行,這樣的情況回存在如下問題。
何時(shí)執(zhí)行Looper.myLooper().quitSafely()
熟悉Looper 的朋友知道,一旦quit之后,Looper 的queue 將無法使用。 而為了使阻塞的@Test線程恢復(fù)運(yùn)行至結(jié)束,又必須在[方案2]的基礎(chǔ)上解除loop().
于是為了滿足這樣的情況,我們只能通過另起一個(gè)HandlerThread 執(zhí)行這種需要跨線程回調(diào)的接口測(cè)試。然后在回調(diào)執(zhí)行完畢前,阻塞最初的測(cè)試線程@Test線程,保證HandlerThread 的存活。(這里我們每次setup 都會(huì)新起一個(gè)線程,原因是,無法跨TestCase 重用這個(gè)線程,當(dāng)Case執(zhí)行完后,該線程會(huì)被系統(tǒng)強(qiáng)制回收)
于是獲得了如下的內(nèi)容
@RunWith(Parameterized.class)
class Test1{
public static final String TAG="sample test";
private HandlerThread t;
private Handler tH;
@Parameterized.Parameters
public static Collection<Object[]> data() {
//測(cè)試數(shù)據(jù)
return Arrays.asList(new Object[][]{
{"TES-1085-7", "TES-1085-7"},
{null, null},
});
}
@Before
public void setUp() throws Exception {
t = new HandlerThread("test");
t.start();
tH = new Handler(t.getLooper());
}
@Test
public void test1(){
final CountDownLatch mutex = new CountDownLatch(1);
tH.post(new Runnable(){
@Override
public void run(){
new YourAsyncJob().run(new YourAsyncTestCallback(){
@Override
public void onFinished(){
Log.i(TAG, "async call back");
mutex.countDown();
}
}
});
Log.i(TAG, "run async ok");
});
mutex.await();
}
}
4 優(yōu)化及處理異常
[方案3]基本能夠處理一般的批量測(cè)試。但是作為一個(gè)嚴(yán)謹(jǐn)?shù)某绦騿T,這樣的代碼顯然是不夠優(yōu)雅的。于是我們需要二次封裝,封裝后的代碼調(diào)用會(huì)簡(jiǎn)潔很多,如下
@RunWith(Parameterized.class)
class Test1 extend ZCCBase{
public static final String TAG="sample test";
@Parameterized.Parameters
public static Collection<Object[]> data() {
//測(cè)試數(shù)據(jù)
return Arrays.asList(new Object[][]{
{"TES-1085-7", "TES-1085-7"},
{null, null},
});
}
@Before
public void setUp() throws Exception {
super.setUp();
}
@Test
public void test1(){
runAsyncTest(new AsyncTest(){
@Override
public void onRun(){
new YourAsyncJob().run(new YourAsyncTestCallback(){
@Override
public void onFinished(){
onAsyncTestFinished();
}
}
});
}
}
是不是優(yōu)雅了很多,具體框架和demo使用可以參考 我的github
還沒完,我們還剩下一個(gè)問題。實(shí)際操作時(shí),異步線程中的Assert Error 如果直接拋出的話,并不能在Android Studio 的Run Text 窗口中直接顯示出來,而是會(huì)顯示成 進(jìn)程crash 的日志,真實(shí)原因需要去logcat 中查找。這顯然不是健全的,因此我們還需要把對(duì)應(yīng)的Throwable 拋回測(cè)試線程。 這一功能也已經(jīng)封裝在 我的github中。