欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

深入探讨Unit Testing in Android

程序员文章站 2023-11-20 17:35:58
1. testing for contentprovider在你开始为provider写case之前,应该仔细读一读sdk文档中关于provider测试的说明。但是光读那些...
1. testing for contentprovider
在你开始为provider写case之前,应该仔细读一读sdk文档中关于provider测试的说明。但是光读那些说明,你还是没办法写出正确的case,因为你也知道,android的文档是比较差劲的,有一些关键东西文档中没有说明,你也知道,这在android当中并不少见。
你写个provider的case,如下:
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
}

编译有错误,它说providertestcase2没有隐式的构造,看来我们需要一个构造函数,写一个标准的junit构造吧!
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
    public feedprovidertest(string name) {
        super(name);
    }
}

wtf,还是有编译错误,而且更严重!难道providertestcase2不是继承自testcase,用了eclipse的建议,它创建了一个带有二个参数的构造:
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
    public feedprovidertest(string name) {
        super(name);
    }

    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
        // todo auto-generated constructor stub
    }
}

但是仅一个名字的feedprovidertest(string name)还是有错误,再试试不带参数的,还是不行,这说明providertestcase2没有这样的构造函数,但是没有道理啊,因为它毕竟是继承自testcase的啊!很神奇和诡异啊!
既然providertestcase2没有一个参数的构造,那么只能去掉带有一参数的构造了!
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
    }

    public void testconstructor() throws throwable {
        assertnotnull("can construct resolver", getmockcontentresolver());
        contentprovider provider = getprovider();
        assertnotnull("can instantiate provider", provider);
    }
}

写了一个基本的测试,运行了下,得到了一个warning,是由junit framework报出来的说demoprovidertest没有定义公共的构造函数testcase(name)或testcase(),什么情况,不是我不定义而是有编译错误啊,因为该死的providertestcase2没有这二个构造!该死,只能再把这个构造加回来!但是因为父类没有,只能引用父类的双参数的构造了!
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> { 
    public demoprovidertest() {
        super(null, null);
    }

    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
    }

    public void testconstructor() throws throwable {
        assertnotnull("can construct resolver", getmockcontentresolver());
        contentprovider provider = getprovider();
        assertnotnull("can instantiate provider", provider);
    }
}

但是参数传什么呢?先用null试试中吧!完全有错误,在父类的构造初始化时出现了npe,这说明传null肯定是不对的!看了下强加的带有二个参数的构造demoprovidertest(class<feedprovider> providerclass, string providerauthority),也说应该传一个class对象,和provider的authority,再试试看!
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
    public demoprovidertest() {
        super(feedprovider.class, authority);
    }

    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
    }

    public void testconstructor() throws throwable {
        assertnotnull("can construct resolver", getmockcontentresolver());
        contentprovider provider = getprovider();
        assertnotnull("can instantiate provider", provider);
    }
}

这次okay了,但是这样一来二个参数的构造就没有意义了,于是让一个参数的调用二个参数的:
复制代码 代码如下:

    public demoprovidertest() {
        this(feedprovider.class, authority);
    }

还是okay,这说明我们的case必须给providertestcase2提供正确的构造参数!
再加上setup和teardown:
复制代码 代码如下:

    @override
    public void setup() throws exception {
        mcontentresolver = getmockcontentresolver();
    }

    @override
    public void teardown() throws exception {
        mcontentresolver = null;
    }

运行,发现testconstructor挂了,说getmockcontentresolver()返回的是null,这怎么可能啊,太诡异了!想到还是可能初始化未正确,给setup加上了父类的调用:
复制代码 代码如下:

    @override
    public void setup() throws exception {
        super.setup();
        mcontentresolver = getmockcontentresolver();
    }

    @override
    public void teardown() throws exception {
        super.teardown();
        mcontentresolver = null;
    }

这下再跑,全都okay了,说明凡是涉及到重写(override)父类的方法,都要调用父类的方法,以期正确初始化!下面是正确的完整版:
复制代码 代码如下:

public class demoprovidertest extends providertestcase2<feedprovider> {
    private contentresolver mcontentresolver;

    public demoprovidertest() {
        this(feedprovider.class, authority);
    }

    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
    }

    @override
    public void setup() throws exception {
        super.setup();
        mcontentresolver = getmockcontentresolver();
    }

    @override
    public void teardown() throws exception {
        super.teardown();
        mcontentresolver = null;
    }

    public void testconstructor() throws throwable {
        assertnotnull("can construct resolver", getmockcontentresolver());
        contentprovider provider = getprovider();
        assertnotnull("can instantiate provider", provider);
    }
}

总结一下,从这个例子得到的经验是,对于组件的测试,都要继承自android.test.*下面的组件测试框架,但是需要给这些组件测试框架传递正确的参数,否则case无法测试:
二个构造函数
复制代码 代码如下:

    public demoprovidertest() {
        this(feedprovider.class, authority);
    }

    public demoprovidertest(class<feedprovider> providerclass,
            string providerauthority) {
        super(providerclass, providerauthority);
    }

一个都不能少,而且是junit的指定构造函数(带有一个string,或不带参数的)要调用测试架构指定的构造,以给测试框架传递正确的参数!
还有就是重写的父类方法时,一定要把父类的方法也调用上,否则还是不会初始化正确!
但是这里不得不说这些组件测试框架写的真是不好用,首先,那个名字就让人费解,为什么有个2啊!android真够2的!还有,既然作为框架,应该把初始化的工作做完整,做彻底,这样才能称的上框架。使用者应该只需要继承,把自己的事情做完,就应该能进行工作,就像组件activity或contentprovider一样,到了你的代码里的时候,框架里的初始化工作已经做完,所以你,继承者只需要关心你自已的初始化工作就好!但是测试框架就烂,继承者不但要关心自己的初始化还要保证给父类传递正确的参数!
2. testing for activity
同样对于activity的测试也是要注意初始化的部分,只不过对于setup和teardown你不调super也没有关系!
复制代码 代码如下:

public class exploreractivitytester extends
        activityinstrumentationtestcase2<exploreractivity> {
    public exploreractivitytester() {
        this(target_package_name, exploreractivity.class);
    }

    public exploreractivitytester(string pkg, class<exploreractivity> class1) {
        super(pkg, class1);
    }

    @override
    public void setup() {
        minstrumentation = getinstrumentation();
    }
}

3. obstacles to unit testing
在android里面,由于其系统架构的特性决定了给android写单元测试用例和验证测试用例特别因难
a. activity reuse
原因就是每一个测试的包,测试的包也是一个apk,每一个包只能注入一个目标apk,也就是说只能针对一个apk里面的内容进行测试,一旦某个操作跳到了apk以外的地方,就超出了测试框架的控制范围。但是组件重用机制在android中非常的普遍,通过intent来跳到其他的应用(apk)中,调用其他应用的组件来完成某个操作,这是android的特性,是再普遍不过的了!但这就给单元测试用例埋下了无法逾越的障碍。测试框架本身更弱,一但跳出了某个组件,instrumentation便无法对其进行控制,开源测试框架robutium-solo一定程度上解决了这个问,solo可以操作一个包内的任何组件,特别地它能够解决多个activity跳转的问题,但是如前所述,因为一个测试apk只能注入一个目标apk,所以一旦activity跳到了应用外,solo也没有了办法。这是一个无解的问题。因此,android当中做测试,只能关注一些逻辑层,api层,数据和provider,service等一些与表层操作较远的代码!对于表层activity跳来跳去的情况,只能做部分测试,或用mockobject来解决,但是这通常失去了测试的本身意义,因为要花大量时间去创建mockobject,不值!
b. actionbar is not clickable
还有一非常恶心的问题是,对于activity的actionbar无法直接点击,真的不明白google到底在搞什么,弄出来个新东东,竟然测试框架里面不支持操作!想到点击actionbar只能通过solo来点击屏幕坐标,这非常难以移植和维护!
说到操作,还不得不说原生框架instrumentation支持的操作非常少,而且不好用,它只能派发keyevent事件,很多情况下都不好用,比如有个对话框,想要点击okay或是cancel的话,就很麻烦,再如想点击一个listview中的某一项的话也是非常麻烦!同样第三方的robotium-solo框架就好用多了,它进行了很好的封装,通过solo.clickontext()就可以方便的点击屏幕上的带有此文字的view。它的内部实现方式是通过view的显示tree,根据tag(文字)来查找相关的view,然后对其发送点击事件!这也解释了为什么solo也无法点击actionbar,因为actionbar不是在activity的view中,它是像statusbar一样,属于系统级别的东西!
c. statusbar belongs to settings.apk
难以想象吧,随处可见的statusbar竟然以属于settings,只有注入了settings的包才能对statusbar进行操作。所以虽然statusbar上面有你的apk的相关的东西(比如提示)但是你还是无法直接操作它,除非你写一个专门注入settings.apk的测试包!
4. security concern
测试的代码(instrumentation和testrunner)也是以一个apk的形式存在的,它可注入任何目标apk,然后就可以对其进行操作,甚至获取其资源和数据。这就带来了安全上面的问题!可以把一个带有测试代码的apk当成一个应用,一旦在某个手机运行,但可以操作任何一个应用。
其实,这本来不是问题,如果应用市场能对开发者上传的应用进行严格的测试和审核。但是现在的问题是无论是google play还是其他市场都不怎么测试,所以就会让不良者有机可乘!
其实,这里的关键问题在于,android厂商不要盲目的追求数量!把应用集中销售是apple想出来的主意,apple的app store也是做的最好的!android只是一个效仿者,所以你发展的慢,数量不多,质量不够,收入不好,是正常的,因为你是一个追随者,你起步晚!对于厂商来讲,数量你没有办法控制,无法一下子弄出几万个应用来,这个是需要时间的,但是,至少,你可以严格控制质量啊!你可以做到对上传的应用进行严格的测试,这是对用户负责,也是对自己负责啊!所以无论是设备还是应用程序,都是apple的要优质一些,android总是要残次一些,所以你看apple的东西价格就高,android就便宜,当然价格也是android的唯一优势!现的社会是一分钱一分货,便宜自然就没好货!