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

0 0
原创粉丝点击