测试Android代码逻辑,光有JUnit和Mockito是不够的,假设你使用了TextView的setText,用Mockito框架的话,默认的TextView的getText方法会返回null,如果是简单的代码,使用Mockito的桩设置还可以接受,如果是要测试到Activity的生命周期等一些复杂逻辑就显得比较复杂了。
为了解决这个问题,诞生了Instrumentation、Robolectric等等的测试框架,不过Instrumentation实际上还是要运行代码到平台上测试,耗费大量的时间,介绍的是运行在JVM上的Robolectric测试框架。
Robolectric基本原理
在使用Robolectric之前我们先要明白Robolectric是如何工作的。比如说我们前文说到的TextView,如果我们使用Mockito,他给我们提供的是Mock后的TextView,而Robolectric给我们提供的是ShadowTextView,这个ShadowTextView实现了TextView身上的方法,但他又与Android的运行环境无关,也就是说他可以像使用TextView一样的方法,但不用在平台上运行代码,大大提高测试效率。
集成Robolectric
首先第一步是添加Gradle编译依赖,由于Robolectric本身比较大,所以对于一些功能,它采用add-on的方式,除了核心包其他都是可选添加的,编辑app下的build.gradle文件:
dependencies { testCompile 'org.robolectric:robolectric:3.+' //核心包 testCompile 'org.robolectric:shadows-support-v4:3.+' //支持Support-v4包 testCompile "org.robolectric:shadows-multidex:3.+" //支持Multidex功能 //... }
值得注意的是,要使用Robolectric也要添加JUnit依赖,具体可以回顾一下《Android单元测试之JUnit框架》。
测试运行环境(@RunWith)
还记之前文章说到的JUnit给我们提供一个@RunWith
注解去设置测试运行环境吗?Robolectric提供一个RobolectricTestRunner
的沙盒测试运行环境,注意低版本的Robolectric可能不是这个类名。这个测试环境使用各种Shadow类代替真正的Android对象,从而实现模拟Android App的运行。所以所有需要使用Robolectric的测试类都要加上类注解:
@RunWith(RobolectricTestRunner.class) public class ExampleUtilsTest { //... }
Robolectric配置(@Config)
很多网上的教程都是直接给了一个简单的例子,结果自己运行的时候会发现出现各种问题,所以这里先不给例子,先说一下怎么配置才能正确地运行。
为了方便配置RobolectricTestRunner提供的环境,比如要设置运行的SDK版本,设置包名,自定义Application等等配置,Robolectric提供了一个@Config
注解方便用户配置Robolectric。我们可以从源码中看到@Config可以接受很多参数,下面是几个比较常用的:
- sdk:SDK版本
- manifest:清单文件位置
- buildDir:构建目录
- packageName:包名
- constants:常量设置(一般直接使用BuildConfig)
- shadows:自定义Shadow类
- application:自定义Application类
由于有很多参数,Robolectric为了使用方便提供了很多默认值,通常唯一必须指定的只有constants,因为配置BuildConfig后Robolectric框架会自动完成寻找各种目录和配置包名等等操作。所以需要在使用Robolectric的测试类上加上@Config注解:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class ExampleUtilsTest { //... }
好了,按照之前说的,配置BuildConfig后就会自动完成寻找各种目录和配置包名等等操作,唯独这个AndroidManifest清单文件可能会寻找不到。
AndroidManifest问题
如果你按照其他的教程来操作,很可能运行时就提示:No such manifest file: build\intermediates\bundles\debug\AndroidManifest.xml
,无法找到AndroidManifest.xml。我们来分析分析这个问题。首先由于@Config最终是给RobolectricTestRunner使用的,所以我们打开RobolectricTestRunner的源码,可以找到:
protected List<FrameworkMethod> getChildren() { //... Config config = getConfig(frameworkMethod.getMethod()); AndroidManifest appManifest = getAppManifest(config); //... }
当然我们找到getAppManifest方法发现它采用了ManifestFactory工厂去生产清单文件,找到工厂接口实现类GradleManifestFactory是适合于Android的清单工厂。可以看到里面各种自动寻找目录的逻辑,其中:
String manifestName = config.manifest(); URL manifestUrl = getClass().getClassLoader().getResource(manifestName); if (manifestUrl != null && manifestUrl.getProtocol().equals("file")) { manifest = FileFsFile.from(manifestUrl.getPath()); } else if (FileFsFile.from(buildOutputDir, "manifests", "full").exists()) { manifest = FileFsFile.from(buildOutputDir, "manifests", "full", flavor, abiSplit, type, manifestName); } else if (FileFsFile.from(buildOutputDir, "manifests", "aapt").exists()) { // Android gradle plugin 2.2.0+ can put library manifest files inside of "aapt" instead of "full" manifest = FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit, type, manifestName); } else { manifest = FileFsFile.from(buildOutputDir, "bundles", flavor, abiSplit, type, manifestName); }
我们修改他的前序buildOutputDir参数,这个参数就是对应Config参数buildDir,所以给Config增加一个参数:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, buildDir = "app/build") public class ExampleUtilsTest { //... }
注意相对和绝对路径问题。
Robolectric运行测试
下面我们尝试些一个测试例子,创建一个Activity并且验证它非null:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, buildDir = "app/build") public class ExampleUtilsTest { @Test public void testActivity() throws Exception { MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); Assert.assertNotNull(mainActivity); } }
这个简单的例子我们先不解析为什么这么写,尝试运行,你可能会遇到以下问题: - AppCompatActivity问题 - MultiDexApplication问题
遇到上述问题请先跳到后面看对应的问题章节,如果你无上述问题,但是运行时一直Download.
这个是Robolectric执行不同SDK版本运行时需要对应运行库,由于服务器本身比较慢,经常超时,有可能一直卡住,如果你足够耐心可以等待其下载完成,如果你没有耐心,可以先跳过,看后面的“Robolectric依赖库问题”。
如果上述问题都处理了,就可以看到运行通过了。接下来我们来正式学习Robolectric的用法。
Robolectric依赖库问题
由于Robolectric的依赖库下载经常超时,我们可以改用手动下载方式去解决,先找到C:\Users\(你的用户名)\.m2\repository\org\robolectric\android-all\
目录为需要下载的依赖库位置,可以Maven参考去下载对应版本:http://mvnrepository.com/artifact/org.robolectric/android-all,下载的Jar后先暂停测试进程,然后删除对应的xxx.jar.tmp文件,复制xxx.jar文件进去,重新运行测试即可。
AppCompatActivity问题
如果你使用的Activity是继承自AppCompatActivity,运行的时候会出现java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
问题,遇到这个问题只需要把继承AppCompatActivity的主题改为Theme.Appcompat主题或者他的子主题,比如:
<activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light"> //.... </activity>
MultiDexApplication问题
如果你的Application继承自MultiDexApplication就有可能会出现:java.lang.RuntimeException: Multi dex installation failed.
,那是因为你没有添加shadows-multidex依赖库,可以参考前文说的,添加:
testCompile "org.robolectric:shadows-multidex:3.+"
测试Activity
上面的例子已经展示了通过Robolectric.setupActivity
创建了一个Activity,那么这个setupActivity做了什么呢?下面我们看一下源码,可以看出,实际上setupActivity相当于做了:
Robolectric.buildActivity(MainActivity.class).create().get();
在Robolectric中,Activity的生命周期由ActivityController来控制,使用buildActivity来创建一个ActivityController,通过查看源码可以看到,调用create()方法,实际上顺序调用了performCreate的方法,实际上就是执行了Activity的onCreate方法。而get()则是获取Activity对象。通过查看ActivityController的源码,可以看出他身上的方法和常用Activity的生命周期对应如下:
• create()-->Activity.onCreate()
• start()-->Activity.onStart()
• resume()-->Activity.onResume()
• pause()-->Activity.onPause()
• stop()-->Activity.onStop()
• destroy()-->Activity.onDestory()
还有其他比如onRestart等的可以参考源代码找到。
测试Intent
假设有一个MainActivity,上面的btnNext按钮点击后会跳转到NextActivity,我们利用Robolectric来测试这段代码:
//模拟点击跳转 Button btnNext = (Button) mainActivity.findViewById(R.id.btn_next); btnNext.performClick(); //获取跳转的意图 Intent actual = ShadowApplication.getInstance().getNextStartedActivity(); //期望意图 Intent expected = new Intent(mainActivity, NextActivity.class); //假设一致 Assert.assertEquals(expected.getComponent(), actual.getComponent());
值得注意的是,如果新版Robolectric使用Assert.assertEquals(expected, actual)来直接对比两个Intent,可能会出现不一致现象。所以只能对比他的组件名,后面会介绍另外一个工具帮助我们快速对比。
测试Fragment
Fragment和Activity的测试大同小异,值得注意的是如果使用的兼容包要注意导入,代码如下:
//非V4包写法 BlankFragment blankFragment = Robolectric.buildFragment(BlankFragment.class).get(); //兼容V4包写法 SuppportFragment supportFragment = new SuppportFragment(); SupportFragmentTestUtil.startFragment(supportFragment);//触发Fragment的onCreateView()
非V4包buildFragment产生的也是FragmentController,和Activity的结构大体相似,而V4包的则是FragmentManager,这里不一一分析了。
另外还有测试Service等组件,可以使用对应的buildXxx,比如说使用Robolectric.buildService(Service.class)
来获得ServiceController,剩下的逻辑和上述的测试Activity的大体相同。如果不关心生命周期,可以把组件当做普通类使用测试(不建议)。
测试Toast
这个看上去似乎没有什么必要的工作,实际上这里是想说明一种测试思想。
//... //上面执行了弹出Toast的代码 assertEquals(ShadowToast.getTextOfLatestToast(),"toast content");
这里举这个简单例子是为了简单说明,如果要获取某个类的状态,可以通过其Shadow类来获取,比如AlertDialog可以通过ShadowAlertDialog来获取弹出的AlertDialog等等。这里就不一一说明了。
Application和ShadowApplication
ShadowApplication.getInstance()
和RuntimeEnvironment.application
两个Application有什么区别呢?我们分析源码看看ShadowApplication.getInstance()的源码如下:
RuntimeEnvironment.application == null ? null : shadowOf(RuntimeEnvironment.application)
其中shadowOf是把真实模拟的Application变成Shadow对象,可以提供一些原本没有的方法。例如RuntimeEnvironment.application可以使用getString去获取字符串信息,而ShadowApplication.getInstance()不行,但他可以使用getNextStartedActivity获取下一个启动的Activity等方法。
获取Shadow对象
假设有一个对象,你想获取它的Shadow对象,可以使用Shadows.shadowOf
,例如上文说到的:
ShadowApplication shadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
如果是自定义的Shadow对象则使用Shadow.extract
方法,别急,马上就说明怎么自定义Shadow对象。
自定义Shadow对象
假设有原始类SampleClass,你想要创建他的Shadow对象,并且想修改和扩展它的方法,原始类代码如下:
/** * 原始类 */ public class SampleClass { public String getString(String str){ return str; } }
你可以使用Robolectric提供给的@Implements注解说明原始类,使用@Implementation说明该方法为替换原始类中的方法,另外可以随意扩展方法,代码如下:
@Implements(SampleClass.class) public class ShadowSampleClass { @Implementation public String getString(String str){ return "test"; } /** * 扩展的方法 */ public String getStringEx(){ return "test"; } }
之后你需要@Config下添加shadows参数说明需要使用的Shadow对象,就可以在代码中使用了,运行的时候单元测试中的SampleClass会被替换成ShadowSampleClass,具体代码如下:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, buildDir = "app/build", shadows = {ShadowSampleClass.class}) public class ExampleUtilsTest { @Test public void testShadows() throws Exception { SampleClass sampleClass = new SampleClass(); String original = sampleClass.getString("123"); Assert.assertEquals("test",original); //转换出Shadow对象 ShadowSampleClass shadowSampleClass = Shadow.extract(sampleClass); Assert.assertEquals("test",shadowSampleClass.getStringEx()); }
}
可以发现上述两个assertEquals都是passed的。
Android 单元测试框架 Robolectric 3.0 介绍 (二) - 掘金