美文网首页单元测试
Android单元测试(七):Robolectric介绍

Android单元测试(七):Robolectric介绍

作者: 云飞扬1 | 来源:发表于2017-08-23 22:39 被阅读764次

    前面花了很多篇幅介绍的JUnit和Mockito,它们都是针对Java全平台的一些测试框架。写到这里,咱们开始介绍一个专门针对Android平台的单元测试框架Robolectric。
    Robolectrie官网:http://robolectric.org

    7.1 关于Robolectric

    Android程序员都知道,在Android模拟器或者真机设备上运行测试是很慢的。执行一次测试需要编译、部署、启动app等一系列步骤,往往需要花几分钟或者更久的时间。
    Robolectric框架就可解决这个问题,它实现了一套JVM能运行的Android SDK,从而能够脱离Android环境进行测试,将原来运行一次测试的时间从几分钟缩短到几秒钟。这简直是Android程序员的福音,是不是很赞。

    7.2 测试环境搭建

    1. 在build.gradle中添加以下依赖:
    • 基础配置
    //版本号根据自己的需要进行修改
    testCompile "org.robolectric:robolectric:3.3.2"
    
    • 如果项目里有用到support-v4包,需要如下配置
    testCompile 'org.robolectric:shadows-support-v4:3.3.2'
    
    • 关于MultiDex
      Robolectric是运行在JVM上的,所以也就没有“MultiDex”这回事,如果我们的工程里使用了它,还需如下配置:
    testCompile 'org.robolectric:shadows-multidex:3.3.2'
    

    它hook了MultiDex的实现,让它在install的时候啥也不干。

    1. 通过注解设置测试类的test runner
    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class, sdk = 23)
    public class RobolectricSampleActivityTest {
        //必须指定test runner为RobolectricTestRunner
        //通过@Config注解来配置运行参数
    }
    
    1. Mac及Linux用户必须注意如下配置

    在Mac以及Linux上,第一次使用时,经常会出现一个很诡异的问题,会告诉我们如下错误:

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

    这个时候我们只需点击“Edit Configurations...” -> “Defaults” -> “JUnit”,修改“Working directory”的值为“MODULE_DIR”,就可解决该问题。
    其官网上也有特意说明:http://robolectric.org/getting-started/

    设置JUnit的Working directory
    1. Android Studio 3.0注意事项
      如果Android Studio是3.0及以上版本的,需要加上以下配置,否则运行不起来,会一直报错。
    android {
      testOptions {
        unitTests {
          includeAndroidResources = true
        }
      }
    }
    

    7.3 第一个测试案例

    场景:我们有一个Activity,里面有一个Button,点击该Button跳转到另外一个Activity。
    测试目的:验证用户点击该Button后,确实是跳转到了我们指定的Activity。
    具体的布局文件以及Activity代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    
        <Button
            android:id="@+id/btn_main"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="unit test"
            />
    
    </RelativeLayout>
    
    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //点击Button后,跳转到RobolectricSampleActivity这个界面
            findViewById(R.id.btn_main).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(MainActivity.this, RobolectricSampleActivity.class);
                    startActivity(intent);
                }
            });
        }
    }
    

    测试代码如下:

    @RunWith(RobolectricTestRunner.class)
    @Config(constants = BuildConfig.class)
    public class MainActivityTest {
    
        @Test
        public void testClickBtnShouldStartSampleActivity() {
            MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
            mainActivity.findViewById(R.id.btn_main).performClick();
    
            Intent expectedIntent = new Intent(mainActivity, RobolectricSampleActivity.class);
            Intent actualIntent = shadowOf(mainActivity).getNextStartedActivity();
            Assert.assertEquals(expectedIntent.getComponent(), actualIntent.getComponent());
        }
    
    }
    

    第一次运行需要比较长的时间,主要是从远程仓库下载robolectric所需的依赖库到本地,请耐心等待即可。

    7.4 @Config配置

    可以通过@Config注解来配置Robolectric运行时的行为。这个注解可以用来注释类和方法,如果类和方法同时使用了@Config,那么方法的设置会覆盖类的设置。如果你有很多测试类都采用同样的配置,那么你可以创建一个基类,通过@Config注解配置该基类,那么其他子类都能共享该配置。

    7.4.1 配置constants
    @Config(constants = BuildConfig.class)
    

    使用@RunWith(RobolectricTestRunner.class)时,必须要指定@Config(constants = BuildConfig.class),这样它会从build/intermediates/目录下找到manifest、assets、resource等目录并加载相应的资源。


    如上图所示,指定该配置后,Robolectric才会加载图中所示文件夹中对应的资源,否则就无法加载manifest、assets、resource等资源,测试也跑不起来。

    7.4.2 配置SDK版本
    @Config(sdk = 23)
    
    7.4.3 配置Application
    @Config(application = BaseApplication.class)
    

    前面说过,在方法上也可以使用@Config来配置,如果类级别与方法级别同时有@Config配置,方法级别上的配置会覆盖类级别的配置。

    7.4.4 配置resource、assets、manifest路径

    前面介绍配置constants属性时,Robolectric会自动加载build/intermediates目录下的资源文件,可以使用以下配置使Robolectric加载特定的资源文件。

    @Config(assetDir = "some/build/path/assert",
            resourceDir = "some/build/path/resourceDir",
            manifest = "some/build/path/AndroidManifest.xml)
    

    这里的路径很容易令人迷惑,必须要说明几点:

    • 如果使用了@Config(constants = BuildConfig.class),资源文件的路径会固定为build目录。避免constants配置与自定义manifest配置一起使用,否则后者配置会不生效。
    • manifest设置的目录base于Unit Test Config里面的”Working Directory”,具体如7.2里的图“设置JUnit的Working directory”所示。
    • resourceDir、assetDir的目录base于manifest的父目录。
        @Config(manifest = "src/test/AndroidManifest.xml", assetDir = "assetDir")
        @Test
        public void testConfigAssetDir() {
            Application app = RuntimeEnvironment.application;
            try {
                InputStream inputStream = app.getAssets().open("test.txt");
                int length = inputStream.available();
                byte[] buffer = new byte[length];
                inputStream.read(buffer);
                inputStream.close();
                String txt = new String(buffer);
                System.out.println(txt);
                inputStream.close();
            } catch (IOException e){
                e.printStackTrace();
            }
        }
    
    自定义manifest目录示意图

    该例子配置了一个指定的AndroidManifest.xml文件以及assets文件目录,测试程序读取assets里test.txt文件内容并打印出来。manifest的相对路径就是“UnitTest”工程里的“app”模块所在的文件路径,assetDir的相对路径就是AndroidManifest.xml文件的父目录路径。

    7.4 Activity测试

    7.4.1 生命周期

    修改下7.3里面的测试案例代码如下,在各个生命周期回调中修改Button的文本内容。

    public class MainActivity extends Activity {
    
        Button mBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mBtn = (Button) findViewById(R.id.btn_main);
            mBtn.setText("onCreate");
            mBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(MainActivity.this, RobolectricSampleActivity.class);
                    startActivity(intent);
                }
            });
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            mBtn.setText("onStart");
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            mBtn.setText("onResume");
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            mBtn.setText("onPause");
        }
    
        @Override
        protected void onStop() {
            super.onStop();
            mBtn.setText("onStop");
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mBtn.setText("onDestroy");
        }
    }
    

    测试代码如下:

        @Test
        public void testActivityLifeCycle() {
            ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
            //会调用Activity的onCreate()方法
            controller.create();
            Button btn = (Button) controller.get().findViewById(R.id.btn_main);
            System.out.println(btn.getText().toString());
            controller.start();
            System.out.println(btn.getText().toString());
            controller.resume();
            System.out.println(btn.getText().toString());
            controller.pause();
            System.out.println(btn.getText().toString());
            controller.stop();
            System.out.println(btn.getText().toString());
            controller.destroy();
            System.out.println(btn.getText().toString());
        }
    

    控制台打印结果如下所示:

    onCreate
    onStart
    onResume
    onPause
    onStop
    onDestroy
    
    7.4.2 setupActivity()与buildActivity()

    前面的示例中看到有2种创建Activity的方式:

    //直接创建一个Activity,创建后的Activity会经历onCreate()->onStart()-onResume()这几个生命周期
    MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
    
    //创建一个ActivityController,然后需要自己手动控制Activity的生命周期
    ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
    

    这2种方式有什么差别呢,查看setupActivity()的源码可以看到:

      //实际上是调用了buildActivity()来创建Activity
      public static <T extends Activity> T setupActivity(Class<T> activityClass) {
        return buildActivity(activityClass).setup().get();
      }
    
      //这里手动控制了Activity的生命周期create()->start()->resume()
      public ActivityController<T> setup() {
        return create().start().postCreate(null).resume().visible();
      }
    
    7.4.3 测试Activity的跳转

    在前面7.3中是示例中已经展示了。

    7.4.4 测试Toast
        //点击button弹出toast信息
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "toast sample", Toast.LENGTH_SHORT).show();
            }
        });
    
        @Test
        public void testToast() {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            Button btn = (Button) activity.findViewById(R.id.btn_main);
            btn.performClick();
            Assert.assertNotNull(ShadowToast.getLatestToast());
            Assert.assertEquals("toast sample", ShadowToast.getTextOfLatestToast());
        }
    
    7.4.5 测试Dialog
        @Test
        public void testDialog() {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            Button btn = (Button) activity.findViewById(R.id.btn_main);
            btn.performClick();
            Assert.assertNotNull(ShadowAlertDialog.getLatestAlertDialog());
        }
    
    7.4.6 测试资源文件
        @Test
        public void testApplication() {
            Application app = RuntimeEnvironment.application;
            Context shadow = ShadowApplication.getInstance().getApplicationContext();
            Assert.assertSame(shadow, app);     
            System.out.println(shadow.getResources().getString(R.string.app_name));
        }
    
    7.4.7 测试Fragment
        @Test
        public void testFragment() {
            TestFragment fragment = new TestFragment();
            //该方法会添加Fragment到Activity中
            SupportFragmentTestUtil.startFragment(fragment);
            Assert.assertThat(fragment.getView(), CoreMatchers.notNullValue());
        }
    

    7.5 Shadow Classes

    顾名思义就是影子类,Robolectric定义了很多shadow class,用来修改或者扩展Android OS中类的行为。当一个Android中的类被实例化时,Robolectric会去寻找对应的影子类,如果找到了则会创建一个影子对象并与之相关联。每当Android类中的一个方法被调用时,Robolectric会保证其影子类中相应的方法会被先调用。这对所有的方法都适用,包括static和final类型的方法。

    Shadows.shadowOf(...);
    

    通过该方法几乎可以获取大部分Android类的shadow class,例如:

        @Test
        public void testShadow() {
            MainActivity activity = Robolectric.setupActivity(MainActivity.class);
            Button btn = (Button) activity.findViewById(R.id.btn_main);
            ShadowActivity shadowActivity = Shadows.shadowOf(activity);
            ShadowTextView shadowTextView = Shadows.shadowOf(btn);
        }
    
    Shadow Classes
    public class Company {
    
        public void welcome() {
            System.out.println("method called in Company.");
        }
    
        public void sayHello() {
            System.out.println("say hello in Company.");
        }
    
    }
    
    //通过@Implements注解来声明shadow类
    @Implements(Company.class)
    public class ShadowCompany {
    
        //通过@Implementation注解来标记shadow方法,此方法声明必须与原Company中的方法声明一致
        @Implementation
        public void welcome() {
            System.out.println("method called in ShadowCompany.");
        }
    
    }
    

    Shadows.shadowOf()方法不能作用于自定义shadow class,为了使Robolectric能够识别自定义shadow类,需要采用@Config注解,如下所示:

        //通过shadows配置自定义的shadow class
        @Config(shadows = {ShadowCompany.class})
        @Test
        public void testShadow() {
            Company company = new Company();
            company.welcome();
            company.sayHello();
        }
    

    执行该测试方法,控制台打印结果如下:

    method called in ShadowCompany.
    say hello in Company.
    

    从以上控制台打印结果中可以看到,welcome()方法实际执行的是ShadowCompany中的方法。

    Shadowing Constructors

    如果需要对构造函数进行shadow,必须实现__constructor__方法,并且该方法的参数必须与构造函数的参数一样。我们稍微修改前面的Company类以及ShadowCompany类,对其构造函数进行shadow。

    public class Company {
    
        private String name;
    
        //构造函数有一个参数name
        public Company(String name) {
            this.name = name;
            System.out.println("company constructor");
        }
    
        public void welcome() {
            System.out.println("method called in Company.");
        }
    
        public void sayHello() {
            System.out.println("say hello in Company.");
        }
    
    }
    
    @Implements(Company.class)
    public class ShadowCompany {
    
        //必须实现该方法,参数与构造函数参数一样
        public void __constructor__(String name) {
            System.out.println("constructor in shadow class.");
        }
    
        @Implementation
        public void welcome() {
            System.out.println("method called in ShadowCompany.");
        }
    
    }
    

    这个时候再执行测试代码,返回结果如下,可以看到Company的构造函数并没有执行。

    constructor in shadow class.
    method called in ShadowCompany.
    say hello in Company.
    
    Getting access to the real instance

    有时shadow类需要使用它们关联的真实对象,可以通过@RealObject注解声明一个属性来实现。

    @Implements(Company.class)
    public class ShadowCompany {
    
        //Robolectric会自动设置真实的关联对象
        @RealObject
        private Company company;
    
        @Implementation
        public void welcome() {
            System.out.println("method called in ShadowCompany." + company.getName());
        }
    
    }
    
    自定义shadow要点
    1. @Implements注解指定需要对哪个类进行shadow;
    2. @Implementation指定需要对哪个方法进行替换;
    3. 使用__constructor__来对构造器进行替换;
    4. @RealObject来引用真实的关联对象;

    7.6 Robolectric的参数化测试

    前面介绍JUnit4的时候讲到,JUnit4中有个叫Parameterized的test runner,能够实现参数化测试,同样Robolectric也提供了同样的功能。

    @RunWith(ParameterizedRobolectricTestRunner.class)
    @Config(constants = BuildConfig.class, sdk = 23)
    public class ParameterizedTest {
    
        @ParameterizedRobolectricTestRunner.Parameters
        public static List data() {
            return Arrays.asList(new Integer[][] {
                    {1, 1},
                    {2, 2},
                    {3, 3},
                    {4, 4}
            });
        }
    
        private int i;
        private int j;
    
        public ParameterizedTest(int i, int j) {
            this.i = i;
            this.j = j;
        }
    
        @Test
        public void testParameter() {
            System.out.println("parameter is " + i + ", " + j);
        }
    
    }
    

    运行结果如下:

    parameter is 1, 1
    parameter is 2, 2
    parameter is 3, 3
    parameter is 4, 4
    

    7.7 Robolectric的局限性

    1. 不支持JNI调用。凡是涉及到JNI调用的方法,都不能使用Robolectric来进行单元测试。对于复杂的应用,或多或少都会有JNI调用,可行的方案是设置一个全局变量来控制是否加载so库。

    系列文章:
    Android单元测试(一):前言
    Android单元测试(二):什么是单元测试
    Android单元测试(三):测试难点及方案选择
    Android单元测试(四):JUnit介绍
    Android单元测试(五):JUnit进阶
    Android单元测试(六):Mockito学习
    Android单元测试(七):Robolectric介绍
    Android单元测试(八):怎样测试异步代码

    相关文章

      网友评论

        本文标题:Android单元测试(七):Robolectric介绍

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