今天看啥  ›  专栏  ›  满嘴跑火车的小土匪

浅谈测试之Robolectric

满嘴跑火车的小土匪  · 掘金  ·  · 2019-10-28 06:11
阅读 12

浅谈测试之Robolectric

Robolectric的由来

Robolectric官网

当你在用JUnit测一些android类时,会发现测试失败:java.lang.RuntimeException: Stub!

如果你没关联android源码,你点进去看android相关类,会发现很多方法的实现就是一行throw new RuntimeException("Stub!")。studio只为我们提供了开发、编译Android代码的环境。但studio没有提供运行环境,如果要运行app,只能在模拟器或者真机上,到时这些方法会被替换成android rom里面相同的类的相同方法。

而JUnit只能用于测试能在jvm上跑的纯java代码,所以,哪怕你的代码里,只有一行Log的日志输出,都会抛这样的异常。

也许MVP这些架构,能将大部分的java代码与android代码进行隔离,比如presenter的代码,基本上就是纯java代码。但view层、model层的实现,往往还是有很多android代码,比如view层的控件,model层的数据库(底层实现是android版的SQLite)。

Robolectric测试框架,能一定程度解决了这种困扰。它的设计思路便是通过实现一套jvm能运行的android代码,从而做到脱离android环境进行测试。Robolectric有一些shadow类,使用它们,可以替换掉android相关类,代替它们在jvm上运行。但这种替换,也需要耗费一定的时间。所以,使用Robolectric的测试,运行起来,都需要十秒左右,虽比不上普通的单元测试,但也比跑真机快多了。

Robolectric的集成

Robolectric的集成比较麻烦,如果集成过程中,有遇到的bug,可以参考文末。尽量使用新版本的Robolectric,可以避免很多不必要的麻烦。

1.app的build.gradle下添加依赖

android {
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}

dependencies {
    testImplementation "org.robolectric:robolectric:4.3"
}
复制代码

注意:

1.如果要使用Robolectric4.0以上版本,必须要studio 3.2以上。

2.如果少了includeAndroidResources = true这个配置,用Robolectric测试UI时,会报类似的错误:

android.content.res.Resources$NotFoundException: String resource ID #0x7f0b001f
复制代码

2.Working directory设置

将Working directory的值设置为$MODULE_DIR$

否则在运行测试方法过程中,可能遇见如下异常:

java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml
复制代码

或者如下警告:

No such manifest file: build/intermediates/bundles/debug/AndroidManifest.xml
复制代码

3.VM options设置

在VM options里面添加一行"-noverify"。

否则在运行测试方法过程中,可能遇见如下异常:

java.lang.VerifyError: Expecting a stackmap frame at branch target 384
Exception Details:
  Location:
    cn/jpush/android/service/PushReceiver.<clinit>()V @11: goto
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0x0000000: 1023 bd00 3c59 0312 1710 ffa7 0175 5359
    0x0000010: 0412 0b03 a701 6c53 5905 1216 04a7 0163
复制代码

4.如果运行第一次运行测试,会在控制台看见下面的日志。需要下载相关jar包。如果你半天没有从官网下载完jar包的话,可以尝试到maven库中下载。

Downloading: org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 2K from sonatype
Downloading: org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 98508K from sonatype
复制代码

可以尝试试一下:http://repo1.maven.org/maven2/ 拼接上 org/robolectric/android-all/8.0.0_r4-robolectric-r1/android-all-8.0.0_r4-robolectric-r1.jar,然后用该链接在浏览器上直接下载(http不行的话,试试https),再将下载到的jar包扔到 类似C:\Users\lenovo\.m2\repository\org\robolectric\android-all\8.0.0_r4-robolectric-r1的目录里面。然后重启studio,重新运行测试即可。

Robolectric的使用

1.测试类下注解配置(3.1以前的旧版本用的是@RunWith(RobolectricGradleTestRunner))

@RunWith(RobolectricTestRunner.class)
//通常,@Config这一行配置可以不要
//@Config(constants = BuildConfig.class)
public class RobolectricTest {
      ......
}
复制代码

constants = BuildConfig.class在某些低版本的Robolectric中需要,但在最新的版本中已经不需要了。在低版本的Robolectric中,需要使用该类中的常量来计算Gradle在构建项目时使用的输出路径。如果没有这些值,Robolectric将无法找到合并的manifest, resources和assets。

2.日志输出

只需要在@Before方法里,预先设置ShadowLog.stream = System.out即可,这样,我们代码里的log,单元测试里的log都会重定向,输出到控制台,方便我们调试。如:

@Before
public void setup() {
    ShadowLog.stream = System.out;
}
复制代码

注意:

1)一个好的单元测试,并不需要日志来支撑。即我们要习惯使用断言,而不是通过看日志来判断测试是否成功。有时候,输出日志只是为了更方便我们调试而已。

2)测试时,日志输出设置,在我们自定义的Application里面,是不起效果的。不过在其他地方,不受影响。估计是Robolectric里面源码执行顺序的问题。

3.Application

RuntimeEnvironment.application是Robolectric模拟android环境生成的Application实例。在你使用Robolectric写测试的时候,你随时可以使用它,作为全局Context。

这也侧面说明一个问题,使用Robolectric写测试的时候,Application是一定会被初始化的。所以如果你有自定义的Application,那么Robolectric必定会初始化你的自定义Application。如果你的自定义Application有一些初始化动作,比如第三方库的加载,Robolectric执行不了,可能会抛异常,那就影响测试结果了。这时候,在项目实践里,可以使用下面的配置@Config(application = Application.class),绕过自定义Application,直接让Robolectric初始化系统的Application。

@RunWith(RobolectricTestRunner.class)
@Config(application = Application.class)
public class NotInitYourApplicationTest {
    ...
}

复制代码

项目实践里,我会使用下面的基类,当某个测试类需要使用Robolectric帮助写测试代码时,继承该基类就行了,不用每次都重复前面所说的日志输出、绕过自定义Application等繁琐的配置。其中,RobolectricRule,继承自JUnit的TestRule类,里面封装了日志输出等设置代码。

@RunWith(RobolectricTestRunner.class)
@Config(application = Application.class)
public class RobolectricTest {
    @Rule
    public RobolectricRule mRobolectricRule = new RobolectricRule();
}
复制代码

4.Shadow

Shadow是Robolectric的核心。Robolectric里,定义了大量对应android源码的shadow类。shadow类,如其名“影子”。当一个android类的方法被调用的时候,Robolectric就会尝试寻找该类相应的影子类,替代对应的android类,执行相应的方法。

下面一个简单的自定义Shadow类的例子:

public class Person {
        public String sayHello() {
            return "I'm myself!";
        }
}

@Implements(Person.class)
public class ShadowPerson {
    @Implementation
    public String sayHello() {
        return "I'm shadow!";
    }
}

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowPerson.class}, manifest = Config.NONE)
public class ShadowTest {
    @Test
    public void sayHello() {
        Person person = new Person();
        assertEquals("I'm shadow!", person.sayHello());
    }
}
复制代码

结果显示,person.sayHello()调用到的是ShadowPerson的sayHello()方法。这就是Shadow。

当然,其背后的原理是很复杂的。使用到了MethodHandle。我也看不懂,就不废话了。

可以参考资料:Robolectric Shadow类实现方式探索

5.Android相关测试

在Robolectric4.0之前,Robolectric也可以用于在src/test下面,对activity、fragment等进行测试。但是,功能极其有限,写起activity、fragment相关测试代码,操作也很繁琐。相关的使用,这里就不介绍了,官网自己都把旧版的使用介绍完全剔除了。感兴趣的可以自己上网搜索相关资料。(Robolectric官网的教程,之前每隔几个版本就大变样,无力吐槽)

在Robolectric4.0之后,Robolectric对Android官方测试库进行了兼容。在这以后,推荐使用AndroidX Test的API写测试代码。也就是说你可以在src/test下面,使用Espresso的api写相关测试。(关于Espresso的使用,将会在浅谈测试之Espresso里详细介绍)

1)引入espresso的相关依赖

testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
testImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'androidx.test.ext:junit:1.1.1'
复制代码

2)测试代码

//@RunWith(AndroidJUnit4.class)代替@RunWith(RobolectricTestRunner.class)
@RunWith(AndroidJUnit4.class)
@Config(application = Application.class)
public class MainActivityTest {
    @Rule
    public IntentsTestRule<MainActivity> intentsTestRule = new IntentsTestRule<>(MainActivity.class, true, false);
    @Before
    public void setup() {
        AppComponent appComponent = DaggerAppComponent.builder()
                .build();
        ComponentHolder.setAppComponent(appComponent);

        intentsTestRule.launchActivity(new Intent(ApplicationProvider.getApplicationContext(), MainActivity.class));
    }
    
    @Test
    public void testClickButton() {
        //使用Espresso的API
        onView(withId(R.id.btn_jump)).perform(ViewActions.click());
        intended(IntentMatchers.hasComponent(LoginActivity.class.getName()));

        //在Robolectric里,无法真的跳转到LoginActivity
        onView(withId(R.id.tv_login)).check(doesNotExist());
    }
}
复制代码

注意:

1)使用@RunWith(AndroidJUnit4.class)代替@RunWith(RobolectricTestRunner.class)

2)目前只建议使用Robolectric单独测试Activity、Fragment。参考issue

当在MainActivity里调用startActivity去开启LoginActivity的时候,最终会调用Instrumentation.execStartActivity()方法。但该类有一个对应的shadow类:ShadowInstrumentation。ShadowInstrumentation里同样有个execStartActivity()方法,它会覆盖Instrumentation.execStartActivity()方法,而它并不会真的去启动LoginActivity。

实际上,试验发现,不仅Activity之间的跳转,如果在当前Activity显示一个Dialog,也无法用espresso的api验证Dialog的信息,意思是Dialog也不会真的初始化,显示出来。想想也是,毕竟Robolectric只是一个Unit Testing Framework。所以,个人并不倾向用Robolectric测试Activity、Fragment里面的代码。因为Activity、Fragment的业务逻辑,通常已经抽取到MVP的P层,或者mvvm的vm层。Activity、Fragment里的代码通常都是控件交互、页面交互,已经算是功能测试的范围了。通常会在src/androidTest里面,使用Espresso写相关的功能测试、端对端测试。

那么Robolectric通常用于哪里?可以用在Presenter层、Model层这些地方的单元测试上面,提供一个模拟的Android运行环境,避免一些Android类,影响我们测试。当然,也建议Presenter层、Model层这些地方,尽量避免不必要的Android代码

官方教程:AndroidX Test

6.测试数据库代码

这里使用到的数据框架是room,再配合上rxjava2。

1)添加room的测试支持库

    testImplementation "android.arch.core:core-testing:1.1.0"
复制代码

2)限于篇幅原因,只贴出部分关键的测试代码。

public class PersonDaoTest extends RobolectricTest {
    @Rule
    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();

    private Person tony = new Person("1", "tony", Sex.MALE);
    private Person marry = new Person("2", "marry", Sex.FEMALE);

    private PersonDatabase database;
    private PersonsDao personsDao;

    @Before
    public void setup() {
        //使用内存数据库。数据将会在测试用例运行过程中被保存,在进程结束后消失。
        database = Room.inMemoryDatabaseBuilder(RuntimeEnvironment.application,
                PersonDatabase.class)
                //允许在主线程进行查询。注意,仅用于测试。
                .allowMainThreadQueries()
                .build();
        personsDao = database.personDao();
    }

    @After
    public void clear() {
        database.close();
    }

    @Test
    public void savePerson_getPerson() {
        personsDao.insertPersion(tony);
        personsDao.getPersonById("1")
                .test()
                .assertNoErrors()
                //不会完成。因为room是响应式的,会继续观察数据库的数据变化。
                .assertNotComplete()
                .assertValue(tony::equals);
    }
}
复制代码

7.配合PowerMock使用

参考资料:Using PowerMock

使用Robolectric可能遇到的bug

1.java.lang.UnsupportedOperationException: Robolectric does not support API level 26.

原因:在默认情况下,Robolectric会在你的manifest中指定的targetSdkVersion上运行你的代码。比如使用的Robolectric3.4版本,不支持API level 26,不支持API level15及以下的sdk版本。

解决方法:

1)使用更高版本的Robolectric。

2)使用@Config的sdkminSdkmaxSdk配置属性,指定你的sdk版本。如:

@Config(sdk = { JELLY_BEAN, JELLY_BEAN_MR1 })
public class YourTest {
}
复制代码

参考:官方教程:Configuring Robolectric

2.Robolectric unit tests fail after Multidex

解决方法:

testImplementation "org.robolectric:shadows-multidex:3.4-rc2"
复制代码

3.javax.net.ssl.SSLHandshakeException

异常详细信息:

javax.net.ssl.SSLHandshakeException: 
        sun.security.validator.ValidatorException: 
        PKIX path validation failed: 
        java.security.cert.CertPathValidatorException: 
        Algorithm constraints check failed on signature algorithm: SHA256WithRSAEncryption
复制代码

解决办法:

testImplementation 'org.bouncycastle:bcprov-jdk15on:1.57'
复制代码

参考:issue

后记

Robolectric,在我看来,这是一个有点被神化的测试框架。因为很多博文介绍它,都是类似“在android设备上跑测试用例太慢,而它能在JVM上跑测试用例”。林林总总,很容易让我们陷入误区,以为要尽可能地使用它,并避免在android设备上跑测试用例。

其实不然,它虽然有一定的好处,能帮助我们不用通过注释原有代码里的android类,比如日志输出等,就可以在jvm上运行我们的测试用例。但也有缺陷:

第一,集成困难。尤其是对初学者来说。实在是太多坑了。

第二,有一定的局限性。归根结底,它只是做了一些mock而已。它只是通过实现一套jvm能运行的android代码,从而做到脱离android环境进行测试,并不是真正的android源码。就像带着镣铐跳舞一样。有时候,还是会碰到各种奇奇怪怪的小bug。

所以,对我个人而言,更多的时候都是使用它来配合Mockito、PowerMock测试Presenter、Model里面的方法,避免因为Presenter、Model里使用到了一小部分android代码而影响测试代码的运行。

注意:

1)Robolectric的源码,貌似无法在windows上编译成功。当初找了老半天原因。唔...后来在官网找到了说法:

We develop Robolectric on Mac and Linux. You might be able to figure out how to get it to work on Windows if you really want to for some reason. Good luck.

2)由于篇幅有限,不可能也没必要列出Robolectric的所有测试示例,以及各种使用姿势。

文中的相关测试例子,以及更多的测试例子均可以在UnitTest里面找到。

更多的测试例子,请参考Robolectric源码里面的测试用例,以及官方给出的robolectric-samples

更详细的教程,请参考:Robolectric官网




原文地址:访问原文地址
快照地址: 访问文章快照