Robolectric踩坑指南

作者: 神棄丶Aria | 来源:发表于2018-01-13 18:55 被阅读906次

    一、介绍

    自己百度去吧。

    二、项目配置

    1、针对Android Studio
    在build.gradle中添加:

    android {
        testOptions {
            unitTests {
                includeAndroidResources = true
     }
        }
    }
    
    dependencies {
       
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.robolectric:robolectric:3.6.1'
    }
    

    2、对mac用户
    需要配置默认的JUnit测试运行器配置。否则会出现
    java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml (系统找不到指定的路径。)
    (1)编辑运行配置 Defaults → Android JUnit
    (2)Working directory的值修改为$MODULE_DIR$

    修改MODULE_DIR.png

    3、附加功能包
    Robolectric为了减少外部依赖数量,将shadow包分割成多个功能包,可根据需要添加


    附加包.png

    使用例子:

    testImplementation 'org.robolectric:shadows-support-v4:latest.release'
    

    三、简单测试用例

    1、Activity测试
    (1)初始化

    @RunWith(RobolectricTestRunner.class)
    @Config(manifest = Config.NONE)
    public class MyActivityTest {
    
        MyActivity mMyctivity;
     TextView mTextView;
     Button mButton;
     ActivityController<MyActivity> mActivityController;
    
     @Before
     public void init(){
            mActivityController = Robolectric.buildActivity(MyActivity.class).create();
     mMyctivity = mActivityController.get();
     mTextView = mMyctivity.findViewById(R.id.textView);
     mButton = mMyctivity.findViewById(R.id.login);
    
     }
    }
    

    (2)生命周期测试

    @Test
    public void TestLifeCycle(){
        assertEquals("onCreate",mTextView.getText());
     mActivityController.start();
     assertEquals("onStart",mTextView.getText());
     mActivityController.resume();
     assertEquals("onResume",mTextView.getText());
     mActivityController.visible();
    
    }
    

    (3)UI测试

    @Test
    public void TestUI(){
    
        Button mInverseBtn = mMyctivity.findViewById(R.id.inverseBtn);
     assertTrue(mInverseBtn.isEnabled());
    
     CheckBox mCheckBox = mMyctivity.findViewById(R.id.checkbox);
     mCheckBox.setChecked(true);
     mInverseBtn.performClick();
     assertTrue(!mCheckBox.isChecked());
     mInverseBtn.performClick();
     assertTrue(mCheckBox.isChecked());
    }
    

    (4)Toast测试

    @Test
    public void TestToast(){
    
        mMyctivity.findViewById(R.id.toastBtn).performClick();
     assertEquals(ShadowToast.getTextOfLatestToast(),"test toast");
    }
    

    (5)Dialog测试

    @Test
    public void TestDialog(){
        //与预测结果相反,待定
     mActivityController.start().resume().visible();
     Button mDialogBtn = mMyctivity.findViewById(R.id.dialogBtn);
     assertTrue(mDialogBtn.isEnabled());
     mDialogBtn.performClick();
     AlertDialog lastAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
     assertTrue(lastAlertDialog == null);
    }
    

    (6)跳转测试

    @Test
    public void TestIntent(){
    
        mButton.performClick();
     Intent expectedIntent = new Intent(mMyctivity,MainActivity.class);
     Intent actual = ShadowApplication.getInstance().getNextStartedActivity();
     assertEquals(expectedIntent.getComponent(),actual.getComponent());
    }
    

    (7)资源测试

    @Test
    public void TestResource(){
        Application application = RuntimeEnvironment.application;
     String appName = application.getString(R.string.app_name);
     assertEquals("RobolectricApp",appName);
    }
    

    2、Service测试
    (1)Service代码

    public class MyService extends IntentService{
    
        public MyService(String name) {
            super(name);
     }
        
        @Override
     protected void onHandleIntent(@Nullable Intent intent) {
            SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences("SERVICE",MODE_PRIVATE).edit();
     editor.putString("data","serviceData");
     editor.apply();
     }
    }
    

    (2)测试代码

    @Test
    public void TestService(){
        Application application = RuntimeEnvironment.application;
     MyService myService = Robolectric.setupIntentService(MyService.class);
     myService.onHandleIntent(new Intent());
     SharedPreferences preferences = application
                .getSharedPreferences("SERVICE", Context.MODE_PRIVATE);
     assertEquals(preferences.getString("data",""),"serviceData");
    }
    

    3、BroadcastReceiver测试
    (1)BroadcastReceiver代码

    public class MyReceiver extends BroadcastReceiver {
        @Override
     public void onReceive(Context context, Intent intent) {
            SharedPreferences.Editor editor = context.getSharedPreferences("TEST", Context.MODE_PRIVATE).edit();
     String data = intent.getStringExtra("data");
     editor.putString("data", data);
     editor.apply();
     }
    }
    

    (2)测试代码

    @Test
    public void TestBroadcast(){
        ShadowApplication shadowApplication = ShadowApplication.getInstance();
    
     String action = "com.example.luzeping_sx.BRADCAST";
     Intent intent = new Intent(action);
     intent.putExtra("data","myData");
     assertTrue(shadowApplication.hasReceiverForIntent(intent));
    
     MyReceiver myReceiver = new MyReceiver();
     myReceiver.onReceive(RuntimeEnvironment.application,intent);
     SharedPreferences preferences = shadowApplication.getApplicationContext()
                .getSharedPreferences("TEST", Context.MODE_PRIVATE);
     assertEquals("myData",preferences.getString("data",""));
    
    }
    

    4、API测试
    (1)初始化

    private final String TAG = "ApiTest";
    private Retrofit retrofit;
    private RetrofitService retrofitService;
    
    @Before
    public void setUp(){
        ShadowLog.stream = System.out;
     retrofit = new Retrofit.Builder()
                .baseUrl("https://api.douban.com/v2/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    
     retrofitService = retrofit.create(RetrofitService.class);
    }
    

    (2)api测试

    @Test
    public void TestApi(){
        try {
            Call<Book> call = retrofitService.getSearchBook("边城",null,0,1);
     Response<Book> response = call.execute();
     Gson gson = new Gson();
     Log.d(TAG,gson.toJson(response));
     assertNotNull(response);
     assertNotNull(response.body());
    
     }catch (IOException e){
            e.printStackTrace();
     }
    }
    

    四、注解

    1、@RunWith
    为测试类配置运行器。

    2、@Config
    为测类配置运行时配置。

    如果想对对应包下的测试类进行相同配置,在 src/test/resources 下创建和对应包名相同的文件夹而后再该文件夹下添加 robolectric.properties 文件。

    example:

    # src/test/resources/com/mycompany/app/robolectric.properties
    sdk=18
    manifest=some/build/path/AndroidManifest.xml
    shadows=my.package.ShadowFoo,my.package.ShadowBar
    

    3、配置阿里云镜像仓库
    Robolectric在每次运行的时候都需要更新它的依赖库。就算是科学上网的情况下下载速度依旧不够理想。因此推荐配置阿里云镜像仓库,提升下载速度。
    步骤:
    (1)自定义RobolectricRunner

    public class MyRoboRunner extends RobolectricTestRunner{
        /**
         * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
         * and res directory by default. Use the {@link Config} annotation to configure.
         *
         * @param testClass the test class to be run
         * @throws InitializationError if junit says so
         */
        public MyRoboRunner(Class<?> testClass) throws InitializationError {
            super(testClass);
            // 从源码知道MavenDependencyResolver默认以RoboSettings的repositoryUrl
            // 和repositoryId为默认值,因此只需要对RoboSetting进行赋值即可
            RoboSettings.setMavenRepositoryId("alimaven");
            RoboSettings.setMavenRepositoryUrl("http://maven.aliyun.com/nexus/content/groups/public/");
    
        }
    }
    

    (2)在测试类中配置该运行器

    @RunWith(MyRoboRunner.class)
    public class MyServiceTest {
    

    五、Shadow

    Robolectric定义了很多Shadow类,它们大多扩展或者修改了Android的实现类【例如ShadowActivity,ShadowApplication】。

    测试时,当某个Android类被实例化时,Robolectric框架会优先去搜索它的Shadow类并创建一个Shadow对象来关联它。

    当Android对象的某个方法被调用时,Robolectric会首先调用Shadow类的对应方法(如果有的话)。
    Robolectric测试框架中,它包含了ShadowView、ShadowCanvas等影子类,即当测试运行起来时最终调用的是这些Shadow类。

    ShadowView中覆盖了draw()方法,在draw方法中最终调用的是canvas.draw()


    image.png

    上面也有提到,Robolectric框架中包含ShadowCanvas类,而在该ShadowCanvas中它覆写了Canvas几乎所有的drawXXX()方法,且这些方法中并没有实际去调用Canvas的draw()方法。总而言之,Robolectric测试框架实际上并没有真正的绘制视图,我猜测这也是为什么它可以如此快速运行的原因。

    六、踩的坑

    讲道理,这框架的文档也太少了。
    基本现在用法是白盒测试,对简单依赖的工程进行测试感觉可以很方便,但工程一旦复杂起来测试十分蛋疼。

    1、你的路径可能找不到
    解决方案:重写RobolectricTestRunner的getManifest()方法,将路径写死

    public class MyRoboRunner extends RobolectricTestRunner{
        @Override
        protected AndroidManifest getAppManifest(Config config) {
    
    
            //TODO 因为测试框架找不到工程的这几个文件 所以只能用绝对路径去指定它们。暂时还没有更好的解决方案。
            //这些是我项目自己的路径,用的话记得改成自己的
            String appRoot = "../";
            String resDir = appRoot + "build/intermediates/res/merged/debug/";
            String assetDirt = appRoot + "build/intermediates/assets/debug/";
    
            return new AndroidManifest(Fs.fileFromPath("../AndroidManifest.xml"),
                    Fs.fileFromPath(resDir),Fs.fileFromPath(assetDirt)){
                @Override
                public List<ResourcePath> getIncludedResourcePaths() {
                    List<ResourcePath> paths = super.getIncludedResourcePaths();
                    return paths;
                } 
            };
    
        }
    }
    

    2、文档没有说明,框架里的类你根本不知道怎么用。
    例如:Robo开头的一系列覆盖类。

    3、Application过于复杂,你需要一个ShadowApplication去覆盖你原本的App类。这样就导致了你不仅要写测试用例,你还要编写测试用逻辑。这部分浪费的时间值不值得见仁见智。

    4、三方库导入问题。
    如果你的三方库是在gradle中配置,那配置时不需要任何配置。
    但如果直接运行的话会出现 java.lang.VerifyError: Expecting a stackmap frame at branch target 的异常。
    解决方案:
    配置JVM参数
    (1)打开Run 的 Edit Configuration
    (2)Android Junit 中选择你的运行选项,切记要选中你的运行对象!
    (3)VM options中添加:-noverify

    image.png

    七、总结

    这个框架看起来好像很牛逼。什么不借助虚拟机就能跑界面逻辑,但其实用起来没那么方便,还一堆坑。官方文档还只给了最简单的用例说明。What the fuck...
    偶尔整理整理,其实还是挺不错的。
    哦差点忘了。附上demo地址:https://github.com/assdd215/RobolectricApp

    相关文章

      网友评论

        本文标题:Robolectric踩坑指南

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