Android移动开发狂热者(299402133)

Robolectric踩坑指南

2018-01-13  本文已影响906人  神棄丶Aria

一、介绍

自己百度去吧。

二、项目配置

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

上一篇 下一篇

猜你喜欢

热点阅读