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 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();
}

你可能感兴趣的:(Android单元测试--Robolectric 3.2.2)