美文网首页单元测试
单元测试框架:Robolectric

单元测试框架:Robolectric

作者: Whyn | 来源:发表于2017-10-13 20:22 被阅读81次

    前言

    前面我们介绍了单元测试框架 JUnitMockito 的使用(详情查看:单元测试框架:JUnit单元测试框架:Mockito),对于绝大多数的 Java 方法,上面两个框架的使用基本就能覆盖绝大多数测试用例编写。然而,如果我们要对 Android 代码进行测试,由于 Android 程序是跑在 Dalvik 虚拟机上的,跟普通 Java 代码跑在 JVM 上不同,因此,无法直接在 JVM 上运行 Android 程序。

    Why Robolectric

    由于无法直接在 JVM 上运行 Android 程序,因此平常我们对 Android 应用的测试都是通过直接将应用部署到虚拟机/真机上进行测试,而这个过程要经过打包、dexing、上传到device、安装,运行,打开界面等一系列过程,十分浪费时间,这对单元测试来说是无法忍受的。我们希望的 Android 单元测试是注重流程与实现,易于测试,时间快,耗时短,我们不想经历 dexing、打包、部署 apk 到设备这些过程,我们不需要看见界面是否出现,我们只想要测试相应的代码的逻辑与功能,因此,Robolectric 应运而生。

    Robolectric 是一套单元测试框架,通过 Robolectric,我们可以在 Java 虚拟机(JVM)上对 Android 应用进行测试。

    Robolectric 原理

    Android 应用是运行在 Dalvik 虚拟机上的,而我们平常开发 Android app 是在 JVM 上的,因此Google 为我们开发 Android app 提供了 SDK(软件开发工具集),各个 API-level 对应的 SDK 都有相应的 android.jar 包,通过这个 androdi.jar 包,我们就可以在 JVM 上调用 Android 系统 api,进行 Android app 开发。然而,这个 android.jar 包的作用仅仅是起一个可以编译打包的功能,我们是没办法直接在 JVM 上运行 androdi.jar 的,可以在 android_sdk_home/platforms/ 查看下 android.jar 包内容,你会发现所有方法内部只有一个实现:throw RuntimeException("stub!!”);

    因此,如果我们在单元测试中直接运行 Android 相关测试用例,那运行的时候就会抛出 RuntimeException("stub!!”) 异常。从这里,我们也可以知道为什么这两年 MVP,MVVM 这种框架流行的原因了,一个原因就是为了隔离 Java 层代码与 Android api 代码的耦合,方便单元测试。

    这里要注意的是,当我们将写好的 Android 应用部署到虚拟机/真机时,android.jar 就会被替换成系统真正的实现,因此功能便能得到实现。

    通过上面的讨论,我们可以知道,无法在 JVM 上运行 Android 代码,是因为 JVM 上 Android api 接口内部实现全部为throw RuntimeException("stub!!”);,因此,一个解决方法就是更改 android.jar 内容,真正实现 Android api 接口。而 Robolectric 的实现原理正是如此,Robolectric 为我们实现了一套 JVM 能运行的 Android api,而且是增强型的 Android api,其内部比原生 Android api 增加了更多的方法,方便我们进行调用测试。

    Robolectric 优点

    • 运行 Android 单元测试,无需启动虚拟机/真机
    • 复写 Android 核心库(即 影子类 - Shadow Classes),扩展更多有用的功能。
    • 可以对 Android 多个组件进行测试,比如:
      -- Activity
      -- Service
      -- Broadcast Receiver
      可以对应用资源进行测试,比如:
      -- string.xml
      -- 应用属性配置(Configuration),比如横屏或者竖屏
      -- Styles and themes
      可以进行测试的还有:
      -- 多渠道(Multiple product flavors)

    Integrate

    via Gradle:

    testImplementation "org.robolectric:robolectric:3.4.2"
    //required  Android Studio 3.0 alpha 5
    android {
      testOptions {
        unitTests {
          includeAndroidResources = true
        }
      }
    } 
    

    最新的版本可以在这里查找:Robolertic-newest-version

    更多配置详情,请查看:Getting Started

    如果使用的是 Android Studio,那么在运行测试用例时如果出现错误:

    android.content.res.Resources$NotFoundException: String resource ID #0x7f0b001f
    

    那么,还需进行如下配置:
    在 gradle.properties 中添加内容:android.enableAapt2=false
    For more detailed information,please see here

    如果出现以下错误:

    No such manifest file: build\intermediates\bundles\debug\AndroidManifest.xml
    

    那么,还需进行如下配置:

    Edit Configurations Working directory

    Sample

    • Activity
    1. Activity跳转测试:摘自官方 Demo
      假设布局如下:
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/login"
            android:text="Login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
    </LinearLayout>
    

    我们希望测试当点击按钮时,启动了LoginActivity

    public class WelcomeActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.welcome_activity);
    
            final View button = findViewById(R.id.login);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
                }
            });
        }
    }
    

    我们通过按钮点击启动LoginActivity,但是由于 Robolectric 是一个单元测试框架,它并不会真正启动LoginActivity,所以我们可以通过查看 WelcomeActivity是否发出了正确的intent即可:

    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class) 
    public class WelcomeActivityTest {
    
        @Test
        public void clickingLogin_shouldStartLoginActivity() {
            WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
            activity.findViewById(R.id.login).performClick();
    
            Intent expectedIntent = new Intent(activity, LoginActivity.class);
            Intent actual = ShadowApplication.getInstance().getNextStartedActivity();
            assertEquals(expectedIntent.getComponent(), actual.getComponent());
        }
    }
    

    更多 [Robolertic] Sample,请查看:robolectric-samples

    配置 Robolectric

    • @Config Annotation
      为一个类或者方法进行配置,可以使用注解@Config ,该注解可应用于类和方法上;方法上的注解会覆盖类上注解。
      如果你发现在多个测试用例上注解了相同内容,那么你可以创建一个基类,将注解移到基类上即可。
      @Config(sdk=JELLYBEAN_MR1,
          manifest="some/build/path/AndroidManifest.xml",
          shadows={ShadowFoo.class, ShadowBar.class})
      public class SandwichTest {
      }
    
    • Configurables - 配置属性
    1. Configure SDK Level - 配置 SDK 版本
      Robolectric 默认会以manifest中的targetSdkVersion运行你的测试代码。如果你想要代码运行在其他的 SDK 中,可以使使用 sdkminSdkmaxSdk属性。
    @Config(sdk = { JELLY_BEAN, JELLY_BEAN_MR1 })
    public class SandwichTest {
    
        public void getSandwich_shouldReturnHamSandwich() {
          // will run on JELLY_BEAN and JELLY_BEAN_MR1
        }
    
        @Config(sdk = KITKAT)
        public void onKitKat_getSandwich_shouldReturnChocolateWaferSandwich() {
          // will run on KITKAT
        }
        
        @Config(minSdk=LOLLIPOP)
        public void fromLollipopOn_getSandwich_shouldReturnTunaSandwich() {
          // will run on LOLLIPOP, M, etc.
        }
    }
    
    1. Configure Application Class - 配置 Application
      Robolectric 默认会创建在manifest中指定的Application实例,如果你想要自定义另一个Application实现,可以进行如下设置:
    @Config(application = CustomApplication.class)
    public class SandwichTest {
    
        @Config(application = CustomApplicationOverride.class)
        public void getSandwich_shouldReturnHamSandwich() {
        }
    }
    
    1. Configure Resource and Asset Paths - 配置 ResourceAsset路径
      Robolectric 对于 Gradle 和 Maven,有默认提供的配置,但是它也允许你自己自定义manifestresourceassets的路径。这个特定对于自定义构建系统是十分有用的,你可以自己指定这些属性:
    @Config(resourceDir = "some/build/path/res")
    public class SandwichTest {
    
        @Config(assetDir = "other/build/path/ham-sandwich/res")
        public void getSandwich_shouldReturnHamSandwich() {
        }
    }
    

    更多配置详情,请查看:Configuring Robolectric

    Driving the Activity Lifecycle - 控制 Activity 生命周期

    1. 获取一个初始化的Activity
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();
    

    上面的代码会创建一个MyAwesomeActivity实例,并且经历了生命周期onCreate

    1. 测试事件在onCreate未发生,在onResume发生
    ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
    Activity activity = controller.get();
    // assert that something hasn't happened
    activityController.resume();
    // assert it happened!
    
    1. 模拟使用Intent启动Activity
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();
    
    1. 模拟Activity异常恢复启动
    Bundle savedInstanceState = new Bundle();
    ···
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
        .create()
        .restoreInstanceState(savedInstanceState)
        .get();
    

    更多ActivityController方法说明,请查看文档:ActivityController

    1. 控制Activity生命周期,并执行控件操作
    Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();
    // now you can interacte with the views inside the Activity
    

    上面代码的visible()表达的是Activity的视图可见,visible()调用后我们就可以对Activity视图进行操作。因为在真实的 Android app 中,Activity的视图层级是在onCreate()调用后某个时间后才附着到Activity上的,在此之前,Activity上的视图是不可见的,这意味着你不能对其视图进行点击等操作,Activity视图层级在Activity经历onPostResume()后才会附着到窗口上。与其臆测视图更新为可见,Robolectric 为开发者提供了这种自己控制视图可见性的功能。

    所以,当你想操作Activity界面视图(views)时,你应当在create()后调用visible()

    Using Add-On Modules - 使用附加模块

    为了减小测试应用外部依赖的数量,Robolectric 影子类被切分多个附加模块。Robolectric 主模块只提供了基础 Android SDK 影子类,其他一些类似appcompatsupport library的影子类在其他的附加模块中。下表列举了附件模块影子类包名:

    SDK Package Robolectric Add-On Package
    com.android.support.support-v4 org.robolectric:shadows-support-v4
    com.android.support.multidex org.robolectric:shadows-multidex
    com.google.android.gms:play-services org.robolectric:shadows-play-services
    com.google.android.maps:maps org.robolectric:shadows-maps
    org.apache.httpcomponents:httpclient org.robolectric:shadows-httpclient

    对于上面列举的附加模块最新版本,可以在 Maven 中进行查询。

    参考

    用Robolectric来做Android unit testing

    Robolectric, Unit testing framework for Android

    相关文章

      网友评论

        本文标题:单元测试框架:Robolectric

        本文链接:https://www.haomeiwen.com/subject/okniyxtx.html