深入探讨Unit Testing in Android

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的唯一优势!现的社会是一分钱一分货,便宜自然就没好货!

(0)

相关推荐

  • 深入探讨Unit Testing in Android

    1. Testing for ContentProvider在你开始为Provider写Case之前,应该仔细读一读SDK文档中关于Provider测试的说明.但是光读那些说明,你还是没办法写出正确的Case,因为你也知道,Android的文档是比较差劲的,有一些关键东西文档中没有说明,你也知道,这在Android当中并不少见.你写个Provider的Case,如下: 复制代码 代码如下: public class DemoProviderTest extends ProviderTestCas

  • Android内存泄漏终极解决篇(上)

    一.概述 在Android的开发中,经常听到"内存泄漏"这个词."内存泄漏"就是一个对象已经不需要再使用了,但是因为其它的对象持有该对象的引用,导致它的内存不能被回收."内存泄漏"的慢慢积累,最终会导致OOM的发生,千里之堤,毁于蚁穴.所以在写代码的过程中,应该要注意规避会导致"内存泄漏"的代码写法,提高软件的健壮性. 本文将从发现问题.解决问题.总结问题的三个角度出发,循序渐进,彻底解决"内存泄漏"的问题

  • 基于android中权限的集合汇总

    程序执行需要读取到安全敏感项必需在androidmanifest.xml中声明相关权限请求, 完整列表如下: 1. android.permission.ACCESS_CHECKIN_PROPERTIES    允许读写访问"properties"表在 checkin数据库中,改值可以修改上传( Allows read/write access to the "properties" table in the checkin database, to change

  • Android camera2 判断相机功能是否可控的实例

    Android Camera2 可调功能适配 首先获取到设备等级: Float hardwareLevel = mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); LEGACY < LIMITED < FULL < LEVEL_3. 越靠右边权限越大 •INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY = 2 •INFO_SUPPORTED_HARDWAR

  • Android Paging库使用详解(小结)

    Android分页包能够更轻易地在RecyclerView里面缓慢且优雅地加载数据. 许多应用从数据源消耗数据, 数据源里面有大量的数据, 但是一次却只展示一小部分. 分页包帮助应用观测和展示大量数据的合理数目的子集. 这个功能有如下几个优势: 数据请求消耗更少的网络带宽和系统资源. 即使在数据更新期间, 应用依然对用户输入响应迅速. 添加分页依赖 按照如下代码添加依赖: dependencies { def paging_version = "1.0.0" implementatio

  • 解读ASP.NET 5 & MVC6系列教程(1):ASP.NET 5简介

    ASP.NET 5简介 ASP.NET 5是一个跨时代的改写,所有的功能和模块都进行了独立拆分,做到了彻底解耦.为了这些改写,微软也是蛮 拼的,几乎把.NET Framwrok全部改写了一遍,形成了一个.NET Core的东西. 在.NET Core里一切都是可配置的,包括Session.MVC等功能,而一切可配置的功能都是可以在Nuget上进行下载. 目前ASP.NET 5依旧兼容老的.NET Framwrok,但要在进行跨平台的部署,还是只能使用新改版的.NET Core CLR. 目前的A

  • Python+unittest+requests 接口自动化测试框架搭建教程

    一.Python+unittest+requests+HTMLTestRunner 完整的接口自动化测试框架搭建_00--框架结构简解 首先配置好开发环境,下载安装Python并下载安装pycharm,在pycharm中创建项目功能目录.如果不会的可以百度Google一下,该内容网上的讲解还是比较多比较全的! 大家可以先简单了解下该项目的目录结构介绍,后面会针对每个文件有详细注解和代码. common: --configDb.py:这个文件主要编写数据库连接池的相关内容,本项目暂未考虑使用数据库

  • ASP.NET Core MVC 修改视图的默认路径及其实现原理解析

    本章将和大家分享如何在ASP.NET Core MVC中修改视图的默认路径,以及它的实现原理. 导语:在日常工作过程中你可能会遇到这样的一种需求,就是在访问同一个页面时PC端和移动端显示的内容和风格是不一样(类似两个不一样的主题),但是它们的后端代码又是差不多的,此时我们就希望能够使用同一套后端代码,然后由系统自动去判断到底是PC端访问还是移动端访问,如果是移动端访问就优先匹配移动端的视图,在没有匹配到的情况下才去匹配PC端的视图. 下面我们就来看下这个功能要如何实现,Demo的目录结构如下所示

  • 分享下网站开发人员应该知道的61件事

    不出意料地,他得到了一大堆回答. 通常情况下,你需要把所有人的发言从头到尾读一遍.但是,Stack Overflow有一个很贴心的设计,它允许在问题下方开设一个wiki区,让所有人共同编辑一个最佳答案.于是,就有了下面这篇文章,一共总结出六个方面共计61条"网站开发须知". 我发现,这种概述性的问题,最适合这种集合群智.头脑风暴式的回答方式了.这也是我第一次觉得,Stack Overflow做到了Wikipedia做不到的事.(难怪它最近挤进了全美前400大网站.) 在我的印象中,关于

  • 将MongoDB作为Redis式的内存数据库的使用方法

     基本思想 将MongoDB用作内存数据库(in-memory database),也即,根本就不让MongoDB把数据保存到磁盘中的这种用法,引起了越来越多的人的兴趣.这种用法对于以下应用场合来讲,超实用: 置于慢速RDBMS系统之前的写操作密集型高速缓存 嵌入式系统 无需持久化数据的PCI兼容系统 需要轻量级数据库而且库中数据可以很容易清除掉的单元测试(unit testing) 如果这一切可以实现就真是太优雅了:我们就能够巧妙地在不涉及磁盘操作的情况下利用MongoDB的查询/检索功能.可

随机推荐