Android Junit 单元测试、异步测试方法简介及异步测试框架指南

本文解决的问题

1. 如何使用junit 做Android 单元测试

2. 如何使用junit 做Android 异步接口单元测试

3. 使用作者封装的框架,优雅地用junit 做Android 异步接口单元测试 [doge]

Junit 作为Android Studio 原生支持的测试框架可以很方便的执行单元测试,并且通过注解 @Test 可以直接标记方法为测试case 然后在子线程中执行。 标记为@UiThreadTest 时,测试case 将在 ui线程中执行。

但是由于junit 本身的设计,当每个test方法执行结束时,该方法的运行线程会一并kill掉, 因此对于异步调用的方法,子线程会一并回收,回调函数也无法执行。

举个栗子,以下的测试case 将无法收到回调并会报错

@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

既然测试线程死掉之后对应子任务都会失败,最直接的方案就是直接阻塞对应

1 线程锁

庆幸Java 提供了极其好用的原生api。CountDownLatch 能够直接阻塞线程,等待完成。 当调用 await()方法时,对应线程会阻塞至 countdownlatch 的 count 变为0 时,恢复运行。因此我们得到以下方案

@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 出来了。

然而在实际使用中又遇见了新的问题。

2 Looper 阻塞(Handler thread)

做过sdk的同学可能会遇见这样的需求:

业务端的同学主线程(或handler线程)发起异步请求,执行完后(通过handler)回调至主线程(或handler线程)。

在处理这个问题是,我们也发现 [方案一]中的回调函数事实上也只能在异步线程中执行,而不能切换回发起线程(测试线程)中执行。这显然不能满足我们优雅的异步接口的测试需求。于是我们需要新的方案2 Looper 阻塞 通过looper 阻塞 并且实现回调函数的线程切换。

上述问题的根本就是handler 线程的回调及切换问题,这个时候由于测试线程是没有looper 的,我们需要为它营造一个这样的环境。 同时,既然有looper 的存在, 那么它的自旋功能也就可以满足我们对阻塞的需求,这样的情况下,我们似乎可以直接抛弃掉之前的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();    }}

看起来是可以适应这样的过程,于是开始愉快的测试起来,但是很快,又遇见了新的问题。

3 Handler thread + 封装

自动化测试好处在于,自动的批量地执行测试case。于是在接下来的过程中我们用到了

    @RunWith(Parameterized.class)

@Parameterized.Parameters 注解来执行参数化的批量输入。

于是新的问题出现了,由于实际运行时@Test方法运行在同一个子线程,因此多次Looper.prepare() 显然是不实际的,(会有RuntimeException)。

于是最直接解决的办法是,一开始prepare好么?

事实上也不行,这样的情况回存在如下问题。

何时执行Looper.myLooper().quitSafely()

熟悉Looper 的朋友知道,一旦quit之后,Looper 的queue 将无法使用。 而为了使阻塞的@Test线程恢复运行至结束,又必须在[方案2]的基础上解除loop().

于是为了满足这样的情况,我们只能通过另起一个HandlerThread 执行这种需要跨线程回调的接口测试。然后在回调执行完毕前,阻塞最初的测试线程@Test线程,保证HandlerThread 的存活。(这里我们每次setup 都会新起一个线程,原因是,无法跨TestCase 重用这个线程,当Case执行完后,该线程会被系统强制回收)

于是获得了如下的内容

@RunWith(Parameterized.class)class Test1{    public static final String TAG="sample test";    private HandlerThread t;    private Handler tH;    @Parameterized.Parameters    public static Collection data() {        //测试数据        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 优化及处理异常

[方案3]基本能够处理一般的批量测试。但是作为一个严谨的程序员,这样的代码显然是不够优雅的。于是我们需要二次封装,封装后的代码调用会简洁很多,如下

@RunWith(Parameterized.class)class Test1 extend ZCCBase{    public static final String TAG="sample test";    @Parameterized.Parameters    public static Collection data() {        //测试数据        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();                    }            }        });    }}

是不是优雅了很多,具体框架和demo使用可以参考 我的github

还没完,我们还剩下一个问题。实际操作时,异步线程中的Assert Error 如果直接抛出的话,并不能在Android Studio 的Run Text 窗口中直接显示出来,而是会显示成 进程crash 的日志,真实原因需要去logcat 中查找。这显然不是健全的,因此我们还需要把对应的Throwable 抛回测试线程。 这一功能也已经封装在 我的github中。

That's all, thanks for your time

更多相关文章

  1. 如何在android的jni线程中实现回调
  2. 页面调用ADB操作Android设备
  3. Android(安卓)Jni中使用线程及回调更新UI
  4. 解决Only the original thread that created a view hierarchy c
  5. 关于Zipalign的介绍和使用方法
  6. 一篇文章带你了解 Android(安卓)消息机制的原理!
  7. intellij idea 设置用真机测试android
  8. Android(安卓)Handler
  9. Handler机制分析

随机推荐

  1. Android日语输入法Simeji使用示例
  2. Android(安卓)端如何添加自定义表情
  3. Official Note of Android(安卓)(importa
  4. Android分享图片
  5. Android中Activity的Intent大全
  6. Android蓝牙通信代码
  7. android 酷狗demo_高仿酷狗音乐播放器And
  8. Android腾讯微薄客户端开发十四:首页menu
  9. Android(安卓)使用HTTP(get和post)方式登陆
  10. android上下文 判断两个context是否相同