zoukankan      html  css  js  c++  java
  • Robolectric使用教程

    转载请标明出处:http://blog.csdn.net/shensky711/article/details/53561172
    本文出自: 【HansChen的博客】


    概述

    Android的单元測试能够分为两部分:

    1. Local unit tests:执行于本地JVM
    2. Instrumented test:执行于真机或者模拟器

    假设使用Local測试,须要保证測试过程中不会调用Android系统API。否则会抛出RuntimeException异常,由于Local測试是直接跑在本机JVM的,而之所以我们能使用Android系统API,是由于编译的时候,我们依赖了一个名为“android.jar”的jar包,可是jar包里全部方法都是直接抛出了一个RuntimeException。是没有不论什么不论什么实现的。这仅仅是Android为了我们能通过编译提供的一个Stub!当APP执行在真实的Android系统的时候,由于类载入机制,会载入位于framework的具有真正实现的类。

    由于我们的Local是直接在PC上执行的。所以调用这些系统API便会出错。
    那么问题来了,我们既要使用Local測试,但測试过程又难免遇到调用系统API那怎么办?其中一个方法就是mock objects。比方借助Mockito,第二种方式就是使用Robolectric。 Robolectric就是为解决问题而生的。它实现一套JVM能执行的Android代码。然后在unit test执行的时候去截取android相关的代码调用。然后转到他们的他们实现的Shadow代码去执行这个调用的过程

    怎样使用?

    为项目加入依赖

    testCompile "org.robolectric:robolectric:3.1.4"

    Robolectric在第一次执行时,会下载一些sdk依赖包,每一个sdk依赖包大概50M。下载速度比較慢,用户能够直接在网上下载相应依赖包,放置在本地maven仓库地址中,默认路径为:C:Usersusername.m2 epositoryorg obolectric

    指定RobolectricTestRunner为执行器

    为測试用例加入注解,指定測试执行器为RobolectricTestRunner。注意。这里要通过Config指定constants = BuildConfig.class,Robolectric 会通过constants推导出输出路径。假设不进行配置。Robolectric可能不能找到你的manifest、resources和assets资源

    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class)
    public class MainActivityTest {
    
    }

    什么是Shadow类

    Shadow是Robolectric的立足之本,如其名,作为影子。一定是变幻莫測,时有时无,且依存于本尊。Robolectric定义了大量模拟Android系统类行为的Shadow类,当这些系统类被创建的时候,Robolectric会查找相应的Shadow类并创建一个Shadow类与原始类关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow相应的方法会调用。这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行測试。


    比方,我们能够借助ShadowActivity验证页面是否正确跳转了

        /**
         * 验证点击事件是否触发了页面跳转。验证目标页面是否预期页面
         *
         * @throws Exception
         */
        @Test
        public void testJump() throws Exception {
            // 默认会调用Activity的生命周期: onCreate->onStart->onResume
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            // 触发button点击
            activity.findViewById(R.id.activity_main_jump).performClick();
    
            // 获取相应的Shadow类
            ShadowActivity shadowActivity = Shadows.shadowOf(activity);
            // 借助Shadow类获取启动下一Activity的Intent
            Intent nextIntent = shadowActivity.getNextStartedActivity();
            // 校验Intent的正确性
            assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
        }

    @Config配置

    能够通过@Config定制Robolectric的执行时的行为。

    这个注解能够用来凝视类和方法,假设类和方法同一时候使用了@Config,那么方法的设置会覆盖类的设置。

    你能够创建一个基类。用@Config配置測试參数。这样,其它測试用例就能够共享这个配置了

    配置SDK版本号

    Robolectric会依据manifest文件配置的targetSdkVersion选择执行測试代码的SDK版本号。假设你想指定sdk来执行測试用例。能够通过以下的方式配置

    @Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
    public class SandwichTest {
    
        @Config(sdk = Build.VERSION_CODES.KITKAT)
        public void getSandwich_shouldReturnHamSandwich() {
        }
    }

    配置Application类

    Robolectric会依据manifest文件配置的Application配置去实例化一个Application类,假设你想在測试用例中又一次指定,能够通过以下的方式配置

    @Config(application = CustomApplication.class)
    public class SandwichTest {
    
        @Config(application = CustomApplicationOverride.class)
        public void getSandwich_shouldReturnHamSandwich() {
        }
    }

    指定Resource路径

    Robolectric能够让你配置manifest、resource和assets路径,能够通过以下的方式配置

    @Config(manifest = "some/build/path/AndroidManifest.xml",
            assetDir = "some/build/path/assetDir",
            resourceDir = "some/build/path/resourceDir")
    public class SandwichTest {
    
        @Config(manifest = "other/build/path/AndroidManifest.xml")
        public void getSandwich_shouldReturnHamSandwich() {
        }
    }

    使用第三方Library Resources

    当Robolectric測试的时候。会尝试载入全部应用提供的资源,但假设你须要使用第三方库中提供的资源文件,你可能须要做一些特别的配置。只是假设你使用gradle来构建Android应用,这些配置就不须要做了,由于Gradle Plugin会在build的时候自己主动合并第三方库的资源,但假设你使用的是Maven,那么你须要配置libraries变量:

    @RunWith(RobolectricTestRunner.class)
    @Config(libraries = {
        "build/unpacked-libraries/library1",
        "build/unpacked-libraries/library2"
    })
    public class SandwichTest {
    }

    使用限定的资源文件

    Android会在执行时载入特定的资源文件。如依据设备屏幕载入不同分辨率的图片资源、依据系统语言载入不同的string.xml,在Robolectric測试其中,你也能够进行一个限定,让測试程序载入特定资源.多个限定条件能够用破折号拼接在在一起。

        /**
         * 使用qualifiers载入相应的资源文件
         *
         * @throws Exception
         */
        @Config(qualifiers = "zh-rCN")
        @Test
        public void testString() throws Exception {
            final Context context = RuntimeEnvironment.application;
            assertThat(context.getString(R.string.app_name), is("单元測试Demo"));
        }

    Properties文件

    假设你嫌通过注解配置上面的东西麻烦。你也能够把以上配置放在一个Properties文件之中。然后通过@Config指定配置文件,比方。首先创建一个配置文件robolectric.properties:

    # 放置Robolectric的配置选项:
    sdk=21
    manifest=some/build/path/AndroidManifest.xml
    assetDir=some/build/path/assetDir
    resourceDir=some/build/path/resourceDir

    然后把robolectric.properties文件放到src/test/resources文件夹下,执行的时候,会自己主动载入里面的配置

    系统属性配置

    • robolectric.offline:true代表关闭执行时获取jar包
    • robolectric.dependency.dir:当处于offline模式的时候。指定执行时的依赖文件夹
    • robolectric.dependency.repo.id:设置执行时获取依赖的Maven仓库ID,默认是sonatype
    • robolectric.dependency.repo.url:设置执行时依赖的Maven仓库地址,默认是https://oss.sonatype.org/content/groups/public/
    • robolectric.logging.enabled:设置是否打开调试开关

    以上设置能够通过Gradle进行配置,如:

    android {
    
        testOptions {
            unitTests.all {
                systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
                systemProperty 'robolectric.dependency.repo.id', 'local'
            }
        }
    }

    驱动Activity生命周期

    利用ActivityController我们能够让Activity执行相应的生命周期方法,如:

        @Test
        public void testLifecycle() throws Exception {
            // 创建Activity控制器
            ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
            MainActivity activity = controller.get();
            assertNull(activity.getLifecycleState());
    
            // 调用Activity的performCreate方法
            controller.create();
            assertEquals("onCreate", activity.getLifecycleState());
    
            // 调用Activity的performStart方法
            controller.start();
            assertEquals("onStart", activity.getLifecycleState());
    
            // 调用Activity的performResume方法
            controller.resume();
            assertEquals("onResume", activity.getLifecycleState());
    
            // 调用Activity的performPause方法
            controller.pause();
            assertEquals("onPause", activity.getLifecycleState());
    
            // 调用Activity的performStop方法
            controller.stop();
            assertEquals("onStop", activity.getLifecycleState());
    
            // 调用Activity的performRestart方法
            controller.restart();
            // 注意此处应该是onStart。由于performRestart不仅会调用restart,还会调用onStart
            assertEquals("onStart", activity.getLifecycleState());
    
            // 调用Activity的performDestroy方法
            controller.destroy();
            assertEquals("onDestroy", activity.getLifecycleState());
        }

    通过ActivityController,我们能够模拟各种生命周期的变化。可是要注意,我们尽管能够任意调用Activity的生命周期,可是Activity生命周期切换有自己的检測机制,我们要遵循Activity的生命周期规律。比方。假设当前Activity并不是处于stop状态。測试代码去调用了controller.restart方法,此时Activity是不会回调onRestart和onStart的。

    除了控制生命周期,还能够在启动Activity的时候传递Intent:

        /**
         * 启动Activity的时候传递Intent
         *
         * @throws Exception
         */
        @Test
        public void testStartActivityWithIntent() throws Exception {
            Intent intent = new Intent();
            intent.putExtra("test", "HelloWorld");
            Activity activity = Robolectric.buildActivity(MainActivity.class).withIntent(intent).create().get();
            assertEquals("HelloWorld", activity.getIntent().getExtras().getString("test"));
        }

    onRestoreInstanceState回调中传递Bundle:

        /**
         * savedInstanceState会在onRestoreInstanceState回调中传递给Activity
         *
         * @throws Exception
         */
        @Test
        public void testSavedInstanceState() throws Exception {
            Bundle savedInstanceState = new Bundle();
            Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedInstanceState).get();
            // verify something
        }

    在真实环境下,视图是在onCreate之后的某一时刻在attach到Window上的,在此之前。View是处于不可操作状态的,你不能点击它。

    在Activity的onPostResume方法调用之后。View才会attach到Window之中。可是,在Robolectric之中,我们能够用控制器的visible方法使得View变为可见。变为可见之后。就能够模拟点击事件了

        @Test
        public void testVisible() throws Exception {
            ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
            MainActivity activity = controller.get();
    
            // 调用Activity的performCreate而且设置视图visible
            controller.create().visible();
            // 触发点击
            activity.findViewById(R.id.activity_main_button1).performClick();
    
            // 验证
            assertEquals(shadowOf(activity).getNextStartedActivity().getComponent().getClassName(), SecondActivity.class.getName());
        }

    追加模块

    为了降低依赖包的大小。Robolectric的shadows类成了好几部分:

    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

    用户能够依据自身需求加入以下依赖包,如

    dependencies {
        ... ...
        testCompile 'org.robolectric:robolectric:3.1.4'
        testCompile 'org.robolectric:shadows-support-v4:3.1.4'
        testCompile 'org.robolectric:shadows-multidex:3.1.4'
        testCompile 'org.robolectric:shadows-play-services:3.1.4'
        testCompile 'org.robolectric:shadows-maps:3.1.4'
        testCompile 'org.robolectric:shadows-httpclient:3.1.4'
    }
    

    自己定义Shadow类

    1. Shadow类须要一个public的无參构造方法以方便Robolectric框架能够实例化它。通过@Implements注解与原始类关联在一起
    2. 若原始类有有參构造方法。在Shadow类中定义public void类型的名为__constructor__的方法,且方法參数与原始类的构造方法參数一直
    3. 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解

    以下我们来创建RobolectricBean的Shadow类
    原始类:

    public class RobolectricBean {
    
        String name;
        int    color;
    
        public RobolectricBean(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public int getColor() {
            return color;
        }
    
        public void setColor(int color) {
            this.color = color;
        }
    }

    Shadow类:

    /**
     * 创建{@link RobolectricBean}的影子类
     *
     * @author HansChen
     */
    @Implements(RobolectricBean.class)
    public class ShadowRobolectricBean {
    
        /**
         * 通过@RealObject注解能够訪问原始对象,但注意,通过@RealObject注解的变量调用方法,依旧会调用Shadow类的方法。而不是原始类的方法
         * 仅仅能用来訪问原始类的field
         */
        @RealObject
        RobolectricBean realBean;
    
        /**
         * 须要一个无參构造方法
         */
        public ShadowRobolectricBean() {
    
        }
    
        /**
         * 相应原始类的构造方法
         *
         * @param name 相应原始类构造方法的传入參数
         */
        public void __constructor__(String name) {
            realBean.name = name;
        }
    
        /**
         * 原始对象的方法被调用的时候,Robolectric会依据方法签名查找相应的Shadow方法并调用
         */
        @Implementation
        public String getName() {
            return "Hello, I ma shadow of RobolectricBean: " + realBean.name;
        }
    
        @Implementation
        public int getColor() {
            return realBean.color;
        }
    
        @Implementation
        public void setColor(int color) {
            realBean.color = color;
        }
    }

    Shadow类中訪问原始类的field

    Shadow类中能够定义一个原始类的成员变量,并用@RealObject注解,这样。Shadow类就能訪问原始类的field了,可是注意,通过@RealObject注解的变量调用方法,依旧会调用Shadow类的方法,而不是原始类的方法,仅仅能用它来訪问原始类的field。

    @Implements(Point.class)
    public class ShadowPoint {
        @RealObject private Point realPoint;
        ...
        public void __constructor__(int x, int y) {
            realPoint.x = x;
            realPoint.y = y;
        }
    }

    怎样在測试用例中让Shadow生效

    在Config注解中加入shadows參数,指定相应的Shadow生效

    @RunWith(RobolectricTestRunner.class)
    @Config(shadows = ShadowRobolectricBean.class)
    public class RobolectricBeanTest {
    
        ... ...
    }

    注意。自己定义的Shadow类不能通过Shadows.shadowOf()获取,须要用ShadowExtractor.extract()来获取。获取之后进行类型转换:

    ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean);

    经常使用測试场景

    页面跳转验证

        /**
         * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面
         *
         * @throws Exception
         */
        @Test
        public void testJump() throws Exception {
            // 默认会调用Activity的生命周期: onCreate->onStart->onResume
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            // 触发button点击
            activity.findViewById(R.id.activity_main_jump).performClick();
    
            // 获取相应的Shadow类
            ShadowActivity shadowActivity = Shadows.shadowOf(activity);
            // 借助Shadow类获取启动下一Activity的Intent
            Intent nextIntent = shadowActivity.getNextStartedActivity();
            // 校验Intent的正确性
            assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName());
        }

    UI组件状态验证

        /**
         * 验证UI组件状态
         *
         * @throws Exception
         */
        @Test
        public void testCheckBoxState() throws Exception {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            CheckBox checkBox = (CheckBox) activity.findViewById(R.id.activity_main_check_box);
            // 验证CheckBox初始状态
            assertFalse(checkBox.isChecked());
    
            // 点击button反转CheckBox状态
            activity.findViewById(R.id.activity_main_switch_check_box).performClick();
            // 验证状态是否正确
            assertTrue(checkBox.isChecked());
    
            // 点击button反转CheckBox状态
            activity.findViewById(R.id.activity_main_switch_check_box).performClick();
            // 验证状态是否正确
            assertFalse(checkBox.isChecked());
        }

    验证Dialog

        /**
         * 验证Dialog是否正确弹出
         *
         * @throws Exception
         */
        @Test
        public void testDialog() throws Exception {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
            // 推断Dialog尚未弹出
            assertNull(dialog);
    
            activity.findViewById(R.id.activity_main_show_dialog).performClick();
            dialog = ShadowAlertDialog.getLatestAlertDialog();
            // 推断Dialog已经弹出
            assertNotNull(dialog);
            // 获取Shadow类进行验证
            ShadowAlertDialog shadowDialog = shadowOf(dialog);
            assertEquals("AlertDialog", shadowDialog.getTitle());
            assertEquals("Oops, now you see me ~", shadowDialog.getMessage());
        }

    验证Toast

        /**
         * 验证Toast是否正确弹出
         *
         * @throws Exception
         */
        @Test
        public void testToast() throws Exception {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            Toast toast = ShadowToast.getLatestToast();
            // 推断Toast尚未弹出
            assertNull(toast);
    
            activity.findViewById(R.id.activity_main_show_toast).performClick();
            toast = ShadowToast.getLatestToast();
            // 推断Toast已经弹出
            assertNotNull(toast);
            // 获取Shadow类进行验证
            ShadowToast shadowToast = shadowOf(toast);
            assertEquals(Toast.LENGTH_SHORT, shadowToast.getDuration());
            assertEquals("oops", ShadowToast.getTextOfLatestToast());
        }

    验证Fragment

    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class, application = CustomApplication.class)
    public class MyFragmentTest {
    
        private MyFragment myFragment;
    
        @Before
        public void setUp() throws Exception {
            myFragment = new MyFragment();
            // 把Fragment加入到Activity中
            FragmentTestUtil.startFragment(myFragment);
        }
    
        @Test
        public void testFragment() throws Exception {
            assertNotNull(myFragment.getView());
        }
    }

    验证BroadcastReceiver

    首先看下广播接收器:

    public class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // do something
        }
    }

    广播的測试点能够包括两个方面

    1. 验证应用程序是否注冊了该广播
    2. 验证广播接收器的处理逻辑是否正确,关于逻辑是否正确。能够直接人为的触发onReceive()方法。让然后进行验证
    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class, application = CustomApplication.class)
    public class MyReceiverTest {
    
    
        @Test
        public void restRegister() throws Exception {
            ShadowApplication shadowApplication = ShadowApplication.getInstance();
    
            String action = "ut.cn.unittestdemo.receiver";
            Intent intent = new Intent(action);
    
            // 验证是否注冊了相应的Receiver
            assertTrue(shadowApplication.hasReceiverForIntent(intent));
        }
    
        @Test
        public void restReceive() throws Exception {
    
            String action = "ut.cn.unittestdemo.receiver";
            Intent intent = new Intent(action);
            intent.putExtra("EXTRA_USERNAME", "HansChen");
    
            MyReceiver myReceiver = new MyReceiver();
            myReceiver.onReceive(RuntimeEnvironment.application, intent);
            // verify something
        }
    }

    验证Service

    Service和Activity一样。都有生命周期,Robolectric也提供了Service的生命周期控制器,使用方式和Activity相似,这里就不做详解了

    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class, application = CustomApplication.class)
    public class TestServiceTest {
    
        private ServiceController<TestService> controller;
        private TestService                    testService;
    
        @Before
        public void setUp() throws Exception {
            controller = Robolectric.buildService(TestService.class);
            testService = controller.get();
        }
    
        /**
         * 控制Service生命周期进行验证
         *
         * @throws Exception
         */
        @Test
        public void testLifecycle() throws Exception {
    
            controller.create();
            // verify something
    
            controller.startCommand(0, 0);
            // verify something
    
            controller.bind();
            // verify something
    
            controller.unbind();
            // verify something
    
            controller.destroy();
            // verify something
        }
    }
  • 相关阅读:
    Windows进程通信(2)使用内存映射文件
    VC2010添加头文件目录
    CString(ANSI/Unicode)与string/wstring的安全转换
    1005 ( Number Sequence )
    1004 ( Let the Balloon Rise )
    1003 ( Max Sum )
    CreateMutex
    CloseHandle
    delphi的Frame简单演示
    DLL中显示模式窗体
  • 原文地址:https://www.cnblogs.com/slgkaifa/p/7354609.html
Copyright © 2011-2022 走看看