Robolectric使用
来源:互联网 发布:cad扒管线软件 编辑:程序博客网 时间:2024/05/17 01:52
Robolectric使用
一、为什么使用Robolectric做Android单元测试
安卓的app需要运行在delvik上面,我们开发Android app是在JVM上面,在开发之前我们需要下载各个API-level的SDK的,下载的每个SDK都有一个android.jar的包,这些可以在你的android_sdk_home/platforms/下面看到。当我们开发一个项目的时候,我们需要指定一个API-level,其实就是将对应的android.jar 加到这个项目的build path里面去。这样我们的项目就可以编译打包了。然而现在的问题是,我们的代码必须运行在emulator或者是device上面,说白了,就是我们的IDE和SDK只提供了开发和编译一个项目的环境,并没有提供运行这个项目的环境,原因是因为android.jar里面的class实现是不完整的,它们只是一些stub,如果你打开android.jar下面的代码去看看,你会发现所有的方法都只有一行实现:
throw RuntimeException("stub!!”);
而运行unit test,说白了还是个运行的过程,所以如果你的unit test代码里面有android相关的代码的话,那运行的时候将会抛出RuntimeException(“stub!!”)。为了解决这个问题,现在业界提出了很多不同的程序架构,比如MVP、MVVM等等,这些架构的优势之一,就是将其中一层抽出来,变成pure Java实现,这样做unit testing就不会遇到上面这个问题了,因为其中没有android相关的代码。当我们的项目运行在emulator或者是device上面的时候,android.jar被替换成了emulator或者是device上面的系统的实现,那上面的实现是真正实现了那些方法的,所以运行起来没有问题。
话说回来,MVP、MVVM这些架构模式虽然解决了部分问题,可以测试项目中不含android相关的类的代码,然而一个项目中还是有很大部分是android相关的代码的,所以上面那种解决方案,其实是放弃了其中一大块代码的unit test。
当然,话说回来,android还是提供了他自己的testing framework,叫instrumentation,但是这套框架还是绕不开刚刚提到的问题,他们必须跑在emulator或者是device上面。这是个很慢的过程,因为要打包、dexing、上传到机器、运行起来界面。。。这个相信大家都有体会,尤其是项目大了以后,运行一次甚至需要一两分钟,项目小的话至少也要十几秒或几十秒。以这个速度是没有办法做unit test的。
解决的办法就是使用一个开源的framework,叫robolectric,他们的做法是通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。举个例子说明一下,比如android里面有个类叫TextView,他们实现了一个类叫ShadowTextView。这个类基本上实现了TextView的所有公共接口,假设你在unit test里面写到
String text = textView.getText().toString();。在这个unit test运行的时候,Robolectric会自动判断你调用了Android相关的代码textView.getText(),然后这个调用过程在底层截取了,转到ShadowTextView的getText实现。而ShadowTextView是真正实现了getText这个方法的,所以这个过程便可以正常执行。
除了实现Android里面的类的现有接口,Robolectric还做了另外一件事情,极大地方便了unit testing的工作。那就是他们给每个Shadow类额外增加了很多接口,可以读取对应的Android类的一些状态。比如我们知道ImageView有一个方法叫setImageResource(resourceId),然而并没有一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是不是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你可以用来测试它是不是正确的显示了你想要的Image.
二、Robolectric的配置及简单使用方法(默认在Android Studio环境下)
要使用Robolectric,需要做几步配置工作。
1. 首先需要将它和JUnit4加到你项目的dependencies里面
testCompile 'junit:junit:4.12'testCompile ’org.robolectric:robolectric:3.0
其中的Robolectric的最新版本号可能会变,具体可以上jcenter查看一下当前的最新版本号。
2. 将Build Variant里面的Test Artifact选择为Unit Test
如果你找不到Build Variant,可以在菜单栏选择View -> Tool Windows -> Build Variant. 正常情况下它会出现在左下角。
3. 如果是Mac的话,还需要配置一个东西
菜单栏选择 Run -> Edit Configuration -> Defaults -> JUnit,在Configuration tab将working directory改成
$MODULE_DIR$。
4. 将Build Variant里面的Test Artifact选择为Unit Test
如果你找不到Build Variant,可以在菜单栏选择View -> Tool Windows -> Build Variant. 正常情况下它会出现在左下角。
5. 对应的测试类,MainActivityTest的代码:
@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class MainActivityTest { @Test public void testMainActivity() { MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); mainActivity.findViewById(R.id.textView1).performClick(); Intent expectedIntent = new Intent(mainActivity, SecondActivity.class); ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity); Intent actualIntent = shadowActivity.getNextStartedActivity(); Assert.assertEquals(expectedIntent, actualIntent); } }
上面的代码测试的就是当用户点击textView的时候,程序会正确的跳转到SecondActivity。其中
@RunWith(RobolectricGradleTestRunner.class)
表示用Robolectric的TestRunner来跑这些test,这就是为什么Robolectric可以检测到你调用了Android相关的类,然后截取这些调用,转到他们的Shadow类的原因。此外,@Config
用来配置一些东西。
代码中的
MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
用来创建MainActivity的instance,或者说,用来启动这个Activity,当Robolectric.setupActivity
返回的时候,这个Activity已经完成了onCreate、onStart、onResume这几个生命周期的回调了。
mainActivity.findViewById(R.id.textView1).performClick();
用来触发点击事件。
ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
用来获取mainActivity对应的ShadowActivity的instance。
shadowActivity.getNextStartedActivity();
用来获取mainActivity调用的startActivity的intent。这也是正常的Activity类里面不具有的一个接口。
最后,调用
Assert.assertEquals
来assert启动的intent是我们期望的intent。
运行这个unit test,启动命令行,cd到项目的根目录,运行
./gradlew test
几秒钟后,你将看到测试运行的结果
... :app:preCompileReleaseUnitTestJava :app:preReleaseUnitTestBuild UP-TO-DATE :app:prepareReleaseUnitTestDependencies :app:processReleaseUnitTestJavaRes UP-TO-DATE :app:compileReleaseUnitTestJava UP-TO-DATE :app:compileReleaseUnitTestSources UP-TO-DATE :app:assembleReleaseUnitTest UP-TO-DATE :app:testRelease UP-TO-DATE :app:test UP-TO-DATE BUILD SUCCESSFUL Total time: 1.45 secs
三、常用Api解释
1.创建待测试的Activity
LoginActivity loginActivity = Robolectric.buildActivity(LoginActivity.class).create().get();
以上代码用于创建一个待测试的LoginActivity,其中的create()会调用onCreate()方法。
2.用指定Intent创建Activity
有的Activity需要使用Intent传递的参数:
Intent intent = new Intent();intent.putExtra("key", "value");LoginActivity loginActivity = Robolectric.buildActivity(LoginActivity.class).withIntent(intent).create().get();<br/>
3.模拟用户的点击
loginButton = (Button) activity.findViewById(R.id.btn_login);Robolectric.clickOn(loginButton);使用clickOn需要在buildActivity后调用visible方法,否则会得到一个错误。activity = Robolectric.buildActivity(LoginActivity.class).create().visible().get();
4.取得最近启动的Activity
假如测试登录界面,输入正确的用户名密码后,点击登录按钮,跳转到HomeActivity:
@Testpublic void should_goto_HomeActivity_when_login_with_valid_user() throws Exception { username.setText("valid_username"); password.setText("valid_password"); Robolectric.clickOn(loginButton); String nextStartedActivityClass = Robolectric.getShadowApplication().getNextStartedActivity().getClass().getName(); assertEquals(HomeActivity.class.getClass(), nextStartedActivityClass);}
类似的方法有:getNextStartedService(),getNextStopedService()。
5.取得最新的Toast消息
假如测试登录界面, 登录失败时,会使用Toast通知用户:
@Testpublic void should_notify_when_login_failed() throws Exception { username.setText("invalid_user"); password.setText("invalid_password"); Robolectric.clickOn(loginButton); assertEquals("Login failed, please check and try again.", ShadowToast.getTextOfLatestToast());}
四、断言工具:AssertJ-Android
Junit的Assert类自带很多的断言方法,比如常用的有:
- assertEquals()
- assertNotNull()
- assertTrue()
- assertFalse()
那我们为什么还要使用第三方的断言库呢?
我们来看一个场景,假设你的应用在网络断开时会显示一个View来提示用户“网络不可用,请连接网络。”
那么我们来测试这个View有没有显示,如果使用Junit断言是这样:
assertEquals(View.GONE, view.getVisibility());
而使用assertJ是这样:
assertThat(view).isGone();
首先从代码简洁和可读性来说,assertJ就胜了一筹。然后最关键的是,当你的测试失败时,Junit会提示:
Expected: <8> but was: <4>
而assertJ的提示是:
Expected visibility <gone> but was <invisible>
相信你已经明白assertJ的妙处了。
而且assertJ还支持链式api:
assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE);
五、自定义Shadow
Shadow Objects是Robolectric在运行时插入到Android.jar包相应的类中的,它们会实际处理方法的调用,并记录相应的状态,以备在assert的时候进行查询。。请参考官方说明:Robolectric说明
1. 什么时候使用?
产品代码中经常会访问sqlite或者通过网络请求数据。为了保证单元测试的稳定性和运行速度,我们需要隔离这些依赖,那么我们可以使用自定义Shadow。
2. 如何使用?
假如我们的被测代码有如下逻辑:
if (LoginUtil.isOnline()) { // doSomething...} else { // doSomething else...}
如果LoginUtil.isOnline()的实现很复杂,很难控制。我们就需要写一个假的实现来替换真实的实现。以方便我们测试上面的逻辑,这个假的实现就叫Shadow。
3. 实现自定义Shadow
创建一个ShadowLoginUtil.java:
@Implements(LoginUtil.class)public class ShadowLoginUtil { private static boolean isOnline = false; @Implementation public static boolean isOnline() { return ShadowLoginUtil.isOnline; } public static boolean setOnline(boolean isOnline) { ShadowLoginUtil.isOnline = isOnline; }}
有了这个假的实现,我们就可以在测试代码里通过调用ShadowLoginUtil.setOnline(true)
来模拟登录成功的状态。
@RunWith(RobolectricTestRunner.class)public class CustomShadowExample { @Test public void testShadow() throws Exception { ShadowLoginUtil.setOnline(true); assertTrue(LoginUtil.isOnline()); ShadowLoginUtil.setOnline(false); assertFalse(LoginUtil.isOnline()); }}
但如果现在运行测试,是会失败的。因为Robolectric并不知道ShadowLoginUtil的存在,想要起作用,我们还要使用自定义TestRunner。
自定义TestRunner
创建MyTestRunner.java:
import java.util.Arrays;import java.util.List;import org.junit.runners.model.InitializationError;import org.robolectric.RobolectricTestRunner;import org.robolectric.bytecode.ClassInfo;import org.robolectric.bytecode.Setup;import org.robolectric.bytecode.ShadowMap;public class MyTestRunner extends RobolectricTestRunner { public MyTestRunner(Class<?> arg0) throws InitializationError { super(arg0); } /** * List of fully qualified class names backed by custom shadows in the test harness. */ private static final List<String> CUSTOM_SHADOW_TARGETS = Arrays.asList(LoginUtil.class.getName());// 需要被Shadow的类 @Override protected ShadowMap createShadowMap() { return super.createShadowMap() .newBuilder() .addShadowClass(ShadowLoginUtil.class) // Shadow类 .build(); } @Override public Setup createSetup() { return new CustomSetup(); } /** * Modified Robolectric {@link Setup} that instruments third party classes with custom shadows. */ public class CustomSetup extends Setup { @Override public boolean shouldInstrument(ClassInfo classInfo) { return CUSTOM_SHADOW_TARGETS.contains(classInfo.getName()) || super.shouldInstrument(classInfo); } }}
在上面的代码中,我们主要是告诉Robolectric我们想用ShadowLoginUtil来替换LoginUtil。 修改测试代码,使用自定义的TestRunner:
@RunWith(MyTestRunner.class)public class CustomShadowExample { ......}
再次运行测试,就可以顺利通过了。
Demo:https://github.com/jsnow0613/MyRobolectricDemo
参考资料:
Robolectric官网
segmentfault社区
GitBook
- Robolectric使用
- 使用 robolectric 做单元测试
- Robolectric使用教程
- Robolectric
- Robolectric测试框架使用文档
- 使用Robolectric进行Android单元测试
- 使用Robolectric做Android单元测试
- Android中Robolectric的使用
- 单元测试Robolectric的使用详解
- android robolectric 单元测试的简单使用
- 使用Robolectric对Android应用进行单元测试
- Android学习第九篇;robolectric的使用
- Android单元测试(四):Robolectric框架的使用
- 使用Robolectric对android程序实现自动化测试
- Android Studio使用Robolectric对android程序实现单元测试
- Android-使用Mockito、Robolectric和RxJava及Retrofit进行单元测试
- Android 系列 3.5使用Robolectric和JUnit测试4
- robolectric relative
- 高精度不会的迅速暴击此处
- Mantis 安装与配置
- 达观数据:文本大数据的机器学习自动分类方法
- RETINA 屏幕1px 边框实现
- 嗅探
- Robolectric使用
- Tar包解析的内存优化方案
- YTU 1098: The 3n + 1 problem
- SpringMVC HandlerInterceptorAdapter登陆验证拦截器
- java 命令
- Python使用二分插入排序竟然比直接插入排序快99倍!
- 简单创建ASPX(C#)工程
- 我的MYSQL学习心得(七) 查询
- PHP烂代码救星-之上传附件《读thinkphp代码有感》