简介
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
解决方案:
- 从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
- 将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 方式:
- 自定义一个用于单元测试的 RoboApplication.class
- 配置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
直接使用SupportFragmentTestUtil
的startFragment
方法启动Fragment。
ProfileFragment mFragment = ProfileFragment
.getInstance(76270);
FragmentTestUtil.startFragment(mFragment);
assertNotNull(mFragment.getView());
而对于support-v4
的Fragment需要使用
SupportFragmentTestUtil
的startFragment
方法启动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了。
- 使用框架提供的Shadow对象
//通过Shadows.shadowOf()可以获取很多Android对象的Shadow对象
ShadowListView shadowListView = Shadows.shadowOf(listView);
shadowListView.performItemClick(0);
- 如何自定义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();
}