Android单元测试--Robolectric 3.2.2

简介


Robolectric通过实现一套JVM能运行的Android代码,从而做到脱离Android环境进行测试。在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。因为不需要再真机上运行,所以测试速度相较于Instrumentation Test要快很多。

Robolectric是个非常强大好用的单元测试框架。虽然使用的过程中肯定也会遇到问题,我个人就遇到不少问题,尤其是跟第三方的library比如Retrofit、ActiveAndroid结合使用的时候,会有不少问题,但瑕不掩瑜,我们依然可以用它完成很大部分的unit testing工作。

Robolectric 3.1(目前最先版本为3.2.2)已支持针对非AndroidSdk的类做Shadow,但是不支持Powermock。如果使用3.0的robolectric 就可以支持Powermock, 如果选择3.1以上的版本的robolectric 就可以支持非AndroidSdk的类的Shadow。本方案中采用Robolectric3.2.2进行Android相关的测试。

具体实践


添加依赖

dependencies {       
    testCompile "org.robolectric:robolectric:3.2.2"
    //robolectric针对support-v4的shadows
    testCompile "org.robolectric:shadows-support-v4:3.0"      
}  

Robolectric在第一次运行时,会下载一些sdk依赖包,每个sdk依赖包至少50M,而 https://oss.sonatype.org 服务器比较慢,导致下载速度非常慢。所以最好手动下载所需要的文件。例如,需要下载以下文件:

Downloading: org/robolectric/android-all/6.0.1_r3-robolectric-0/android-all-6.0.1_r3-robolectric-0.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 56874K from sonatype

解决方案:

  1. http://repo1.maven.org/maven2/org/robolectric/android-all/6.0.1_r3-robolectric-0/android-all-6.0.1_r3-robolectric-0.jar中下载android-all-6.0.1_r3-robolectric-0.jar
  2. 将jar文件放置在本地maven仓库地址中,例如:
    C:\Users\Administrator\.m2\repository\org\robolectric\android-all\6.0.1_r3-robolectric-0

重写TestRunner

在非app的module中添加Robolectric测试框架时,很可能会出现以下异常:

java.lang.RuntimeException: build\intermediates\bundles\debug\AndroidManifest.xml not found or not a file; it should point to your project's AndroidManifest.

发生此异常,是因为无法正常获取AndroidManifest文件。此时就需要重写TestRunner,可以从RobolectricGradleTestRunner继承,然后覆盖getAppManifest方法,构建自己想要的特殊的AndroidManifest对象来实现对AndroidManifest,assets,res资源的控制。

public class MyRobolectricTestRunner extends RobolectricTestRunner {
    ... ...
    @Override
    protected AndroidManifest getAppManifest(Config config) {
        int nameLength = projectName.length();
        String rootPath = System.getProperty("user.dir", "./");
        int index  = rootPath.indexOf(projectName); 
        if (index == -1) {
            throw new RuntimeException("project name not found in user.dir");
        }
        //获取项目的根目录
        rootPath = rootPath.substring(0, index + nameLength);
        String manifestProperty = rootPath + "/module/src/main/AndroidManifest.xml";
        String resProperty = rootPath + "/module/src/main/res";
        String assetsProperty = rootPath + "/module/src/main/assets";
        return new AndroidManifest(
            Fs.fileFromPath(manifestProperty), 
            Fs.fileFromPath(resProperty),
            Fs.fileFromPath(assetsProperty)) {
            @Override
            public int getTargetSdkVersion() {
                return MAX_SDK_SUPPORTED_BY_ROBOLECTRIC;
            }
        };
    }
}

隔离原Application依赖

如果用 Robolectric 单元测试,不配置Application ,就会调用原来的项目的Application,而 App 有很多第三方库依赖。于是,执行 App 生命周期时, robolectric 就容易报错。
正确配置 Application 方式:

  1. 自定义一个用于单元测试的 RoboApplication.class
  2. 配置Application。有以下两种方式:

方式一:在单元测试 XXTest 加上 @Config(application = RoboApplication.class) 。

 @Config(application = RoboApplication.class) 
 public class XXXTest { }

方式二:在TestRunner中设置Application

 @Override
 protected Config buildGlobalConfig() {
     return new Config.Builder()
         .setApplication(RoboApplication.class)
         .build();
 }

日志输出

我们在写UT的过程,其实也是在调试代码,而日志输出对于代码调试起到极大的作用。而Robolectric对日志输出的支持其实非常简单。只需要在每个TestCase的setUp( )方法或者Application的onCreate()方法中中添加一句命令:

@Before 
public void setUp() throws URISyntaxException { 
    //输出日志 
    ShadowLog.stream = System.out; 
}

此时,无论是功能代码还是测试代码中的 Log.i()之类的相关日志都将输出在控制面板中。

单元测试示例

测试代码是放在app/src/test下面的,测试类的位置最好跟被测试类的位置对应,比如MainActivity放在 app/src/main/java/com/robo/test/MainActivity.java那么对应的测试类MainActivityTest最好放在app/src/test/java/com//robo/test/MainActivityTest.java

下面以XfConsultantProfileActivity类的测试为例,进行简单的介绍:

新建Unit Test

通过注解配置TestRunner等基本信息

@RunWith(MyRobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class ProfileActivityUnitTest {
    private ProfileActivity mActivity;

    @Before //在运行test之前,进行一些初始化操作
    public void setUp() throws Exception {
    }

    @Test //测试方法
    public void testFragment() throws InterruptedException { 
    }
}

启动Activity

可以通过setupActivity()方法来直接启动Activity

mActivity = Robolectric.setupActivity(ProfileActivity.class);

如果要启动的Activity需要从Intent中获取额外的数据,那么就需要使用buildActivity()方法来获取ActivityController。通过ActivityController,不仅可以设置Activity的Intent,还可以创建Activity和控制Activity的生命周期。

Intent intent = new Intent();
intent.putExtra("id", 76270);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(RuntimeEnvironment.application, DetailActivity.class);
mActivity = Robolectric.buildActivity(ProfileActivity.class)
                .withIntent(intent).setup().get();

验证Fragment

通过Activity对象获取Fragment:

//获取Fragment
List<Fragment> fragmentList = mActivity.getSupportFragmentManager().getFragments();
ProfileFragment mFragment = null;
if (fragmentList.get(0) instanceof ProfileFragment) {
    mFragment = (ProfileFragment) fragmentList.get(0);
}
assertNotNull(mFragment);

也可以脱离Activity,直接启动Fragment实体。对于android.app.Fragment直接使用SupportFragmentTestUtilstartFragment方法启动Fragment。

ProfileFragment mFragment = ProfileFragment
            .getInstance(76270);
FragmentTestUtil.startFragment(mFragment);
assertNotNull(mFragment.getView());

而对于support-v4的Fragment需要使用
SupportFragmentTestUtilstartFragment方法启动Fragment,且需要添加相应的依赖:

dependencies {       
    //robolectric针对support-v4的shadows
    testCompile "org.robolectric:shadows-support-v4:3.2.2"      
}  

验证ListView

//执行ListView的Item点击事件
 ListView listView = (ListView) mFragment.findViewById(R.id.listview);
 View view = listView.getAdapter().getView(1,null,null);
 listView.performItemClick(view,0,0);

验证RecyclerView

RecyclerView recyclerView = (RecyclerView) housesLayout.findViewById(R.id.housesView);
//需要添加下面这两句代码,RecyclerView才会加载布局
recyclerView.measure(0, 0);
recyclerView.layout(0, 0, 100, 10000);
//指向Item点击事件
recyclerView.findViewHolderForAdapterPosition(0).itemView.performClick();

处理网络回调

在后台线程中请求网络,请求完成后在UI线程里通过Listener接口通知请求完成,并传递请求回来的数据。这时需要如何处理呢?

在使用Robolectric框架测试需要在UI线程执行的逻辑时,在Android平台UI线程会轮询消息队列,然后从消息队列里取出消息,并将消息分发给Handler处理,UI线程执行的是轮询消息队列的死循环。但是在Robolectric框架中运行时,UI线程默认情况下并不会轮询消息队列,而需要在测试用例代码里主动驱动UI线程从消息队列里取出消息进行分发。测试用例执行时并不在UI线程,而是在单独的线程中,所以它可以主动驱动UI线程分发消息。

所以在执行网络请求后,需要主要驱动UI线程轮询消息队列,获取返回的数据。从下面的代码可以看到我们可以通过获取Scheduler对象来判断消息队列中是否有消息,并调用Scheduler的runOneTask方法进行消息分发,这样就驱动了主线程进行消息轮询,

//获取主线程的消息队列的调度者,通过它可以知道消息队列的情况
//并驱动主线程主动轮询消息队列
Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
//因为调用请求方法后 后台线程请求需要一段时间才能请求完毕,然后才会通知主线程
// 所以在这里进行等待,直到消息队列里存在消息
while (scheduler.size() == 0) {
    Thread.sleep(500);
}
//轮询消息队列,这样就会在主线程进行通知
scheduler.runOneTask();

Shadow

Shadow是Robolectric的立足之本。因此,框架针对Android SDK中的对象,提供了很多Shadow对象(如Activity和ShadowActivity、TextView和ShadowTextView等),这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。从Robolectric 3.1开始已支持针对非Android SDK的类构建Shadow了。

  1. 使用框架提供的Shadow对象
//通过Shadows.shadowOf()可以获取很多Android对象的Shadow对象
ShadowListView shadowListView = Shadows.shadowOf(listView);
shadowListView.performItemClick(0);
  1. 如何自定义Shadow对象

以User类为例,创建自定义 Shadow对象。

@Implements(User.class) //原始对象
public class ShadowUser {
    @Implementation //重新实现原始对象中的方法
    public long getUserId() {
        return (long)70652;
    }
}

接下来,需将定义好的Shadow对象,在TestRunner进行设置

private static final Class<?> mShadowClass[] = {ShadowUser.class};

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

推荐阅读更多精彩内容