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

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

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

<span id="section1">1 线程锁</span>

庆幸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<Object[]> 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<Object[]> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 第一章:Android基石——四大组件 四大组件:• Activity:负责UI元素的加载与页面之间的跳转,相当于...
    loneyzhou阅读 577评论 0 1
  • 1. ANR异常 Application No Response:应用程序无响应。在主线程中,是不允许执行耗时的操...
    JackChen1024阅读 1,324评论 0 3
  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 19,820评论 13 54
  • 最近工作中遇到一个比较迷惑的事情,在我利用runtime获取类的属性的时候,由于类实现了一个自定义协议,导致遍历出...
    JamesYu阅读 15,882评论 6 28
  • 忆 少 时 夜 宿 杨 家 溪 少时曾随家父往福鼎白琳老家探亲,时交通闭塞,无车可乘,均靠步行。从霞浦城关到福鼎白...
    王世荣阅读 286评论 2 2