阅读 206

Android自动化测试入门(四)单元测试

单元测试一般分两类:

  • 本地测试:运行在本地的计算机上,这些测试编译之后可以直接运行在本地的Java虚拟机上(JVM)。可以最大限度的缩短执行的时间。如果测试中用到了Android框架中的对象,那么谷歌推荐使用Robolectric来模拟对象。
  • 插桩测试:在Android设备或者模拟器上运行的测试,这些测试可以访问插桩测试信息,比如被测设备的Context,使用此方法可以运行具有复杂Android依赖的单元测试。前两篇中的Espresso 和 UI Automator就是这类测试,Espresso一般用来测试单个界面,UI Automator一般用来测试多界面交互。它们运行的比本地测试慢很多,所以谷歌建议最好是必须针对设备测试的时候才使用。

本地单元测试在Android自动化测试中是比重最大的一环,主要针对某个类中的某个方法。谷歌建议在所有的测试中,单元测试要占到70%的比重,为啥它就这么重要呢?

  • 本地单元测试相比于前面几篇中的UI测试执行效率高,前面的UI测试是需要运行在手机上的,所以想要运行测试就需要执行代码的编译、打包、安装、运行,这是非常耗时的,特别是工程很大的时候,运行一次可能需要很长的时间。如果我们只是改变了代码中的一个方法,使用单元测试可以快速验证该方法的正确性。
  • 提高写代码的抽象和封装能力,比如刚入行的时候,我们可能在一个按钮的OnClickListener方法中写一大坨代码,如果了解单元测试就会知道这样写对测试非常不友好,把这一坨提取封装会更利于测试,也就能更快的验证代码的正确性。
  • 因为单元测试是独立的单个方法的测试,那么当测试结果与预期不一致的时候,可以迅速定位bug。
  • 提高代码的稳定性,和易维护性,写代码的时候能确保正确开发,在修改代码之后,保证功能不被破坏,其实编写单元测试的过程也是对代自己写的代码的Code Review,是对代码持续重构的开始。

本部分会用到四个小东西,Junit,Mockito,PowerMockito,Robolectric。Junit是单元测试框架,Mockito和Robolectric都是用来产生模拟对象的,Mockito在Java中用的多,PowerMockito是Mockito的增强版可以模拟final,static,private等Mockito不能mock的方法,Robolectric可以模拟更多的Andorid框架中的对象。

  • 如果要构建的本地单元测试对Android框架依赖小,可以选择mockito,速度更快。
  • 如果要构建的本地单元测试对Android框架有很大的依赖性,可以选择Robolectric

Junit

Junit是java中非常有名的测试框架,让测试变得很容易。假如下面我们有一个toNumber的方法要测试

public class Utils {

    public Integer toNumber(String num){
        if(num == null || num.isEmpty()){
             return null;
        }
        Integer integer;
        try {
            integer = Integer.parseInt(num.trim());
        }catch (Exception e){
            integer = null;
        }
        return integer;
    }
    
}
复制代码

为了保证测试的全面性,我们可能需要设计下面的几个测试用例

  • 如果传入的是null,那么应该返回null
  • 如果传入的全是数字比如"12321",那么应该返回整数12321
  • 如果传入的字符串左边或者右边,或者两边都有空格比如"123 "," 123"," 123 ",那么应该返回正确的整数123
  • 如果传入的字符串中间有空格,或者有字母比如""12 3","12ab",这时候会发生崩溃,我们不让他崩溃,让他返回null

测试代码如下

public class ExampleUnitTest {

    @Test
    public void testToNumber_NotNullOrEmpty(){
        Utils utils = new Utils();
       assertNull(utils.toNumber(null));
       assertNull(utils.toNumber(""));
    }
    @Test
    public void testToNumber_hasSpace(){
        Utils utils = new Utils();
        assertEquals(new Integer("123"),utils.toNumber("123"));
        assertEquals(new Integer("123"),utils.toNumber("123 "));
        assertEquals(new Integer("123"),utils.toNumber(" 123 "));
    }
    @Test
    public void testToNumber_hasMiddleSpace(){
        Utils utils = new Utils();
        assertNull(utils.toNumber("12 3"));
        assertNull(utils.toNumber("12a3"));
    }
}
复制代码

其实写单元测试也是对自己代码的一次检查和重构,比如上面的toNumber方法,第一次写的时候可能有很多问题都没有想到直接返回一个Integer.parseInt()就完事了,随着单元测试写完并且测试用例都通过之后,这个方法也会变的更加健壮,变成了前面代码中所写的那样。

mockito

Junit已经能完成单元测试了,为啥要使用Mockito或者Robolectric?

我们需要明确单元测试的目的:单元测试的目的是为了测试我们自己写的代码的正确性,它不需要测试外部的各种依赖,所以当我们遇到一个方法中有很多别的对象的依赖的时候,比如操作数据库,连接网络,读写文件等等,需要给它解依赖。

怎么解依赖呢?其实就是弄一些假对象,比如代码中是我们从网络获取一段json数据,转化成一个对象传入到我们的测试方法中。那么就可以直接new一个假的对象,并给它设置我们期望的返回值传给要测试的方法就好了,不需要再去请求网络获取数据。这个过程称之为mock

直接手动去new一个对象,然后去设置各种数据是比较麻烦的,而Mockito这类的框架就是用来简化我们手动mock的。使用他们来创建一个虚拟对象设置返回值等操作会变得非常简单。

下面开始练习,测试代码写在 src/main/test/java文件夹下面

先练习使用mockito,引入依赖库

testImplementation 'org.mockito:mockito-core:3.0.0'
复制代码

新建一个MockitoTest类,在类上添加注解@RunWith(MockitoJUnitRunner.class)表示Junit要把测试方法运行在MockitoJUnitRunner上

@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {......}
复制代码

例子1: 结果验证,测试某些结果是否正确,使用when和thenReturn表示当调用某个方法的时候指定返回值。最后通过assertEquals判断返回值是否正确

@Test
    public void testMockitoResult() {
        Person person = mock(Person.class);
        //当调用person.getAge()方法的时候,给它返回一个18
        when(person.getAge()).thenReturn(18);
        //当调用person.getName()方法的时候,给它返回一个Lily
        when(person.getName()).thenReturn("Lily");
        //判断返回跟预期是否一样
        assertEquals(18, person.getAge());
        assertEquals("Lily", person.getName());
    }
复制代码

例子2: 验证行为,有时候会测试某些行为是否被执行过,通过verify方法可以验证某个方法是否执行过,执行的次数

 @Test
    public void testMockitoBehavior() {
        Person person = mock(Person.class);
        int age = person.getAge();
        //验证getAge动作有没有发生
        verify(person).getAge();
        //验证person.getName()是不是没有调用
        verify(person, never()).getName();
        //验证是否最少调用过一次person.getAge
        verify(person, atLeast(1)).getAge();
        //验证getAge动作是否被调用了2次,前面只用了一次所以这里会报错
        verify(person, times(2)).getAge();
    }
复制代码

例子3: 通过Mockito mock一个Person对象,那么这个对象的name属性是默认为null的,如果我们不想让它为null,默认为空字符串可以使用RETURNS_SMART_NULLS

   @Test
    public void testNotNull(){
        Person person = mock(Person.class);
        System.out.println(person.getName());
        Person person1 = mock(Person.class,RETURNS_SMART_NULLS);
        System.out.println(person1.getName());
    }
复制代码

例子4: 可以使用@Mock注解来mock一个对象比如

 @Mock
    List<Integer> mList;
    @Test
    public void testAnnotationMock(){
        mList.add(0);
        verify(mList).add(0);
    }
复制代码

例子5: 可以验证是否执行了某个参数的方法

@Test
    public void testParameter(){
        Person person = mock(Person.class);
        when(person.getDuty(1)).thenReturn("医生");
        System.out.println(person.getDuty(1));
        //anyInt任何Int值,此外还有anyString,anyFloat等
        when(person.getDuty(anyInt())).thenReturn("护士");
        System.out.println(person.getDuty(anyInt()));
        //验证person.getDuty(1)方法有没有调用
        verify(person).getDuty(ArgumentMatchers.eq(1));
    }
复制代码

例子6: mock出来的对象都是虚拟的对象,我们可以验证其执行次数,状态等,如果一个对象是真实的,那怎么验证呢 可以使用spy包装一下

spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。

  @Test
    public void testSpy(){
        Person person = getPerson();
        Person spy = spy(person);
        when(spy.getName()).thenReturn("Lily");
        System.out.println(spy.getName());
        verify(spy).getName();
    }
    private Person getPerson(){
        return new Person();
    }
复制代码

Mockito虽然好用但是也有些不足,比如不能mock static、final、private等对象,使用PowerMock就可以实现了

powermock

powermock官网

首先添加依赖

  testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
  testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
复制代码

创建一个PowerMockTest类,在类上添加注解@RunWith(PowerMockRunner.class),通知Junit该类的测试方法运行在PowerMockRunner中。在添加注解@PrepareForTest(Utils.class)表示要测试的方法所在的类,这里是一个自定义的Utils.class

例子1: 测试static方法

目标方法

 public static boolean isEmpty(@Nullable CharSequence str) {
        return str == null || str.length() == 0;
    }
复制代码

测试方法

  @Test
    public void testStatic(){
        PowerMockito.mockStatic(Utils.class);
        PowerMockito.when(Utils.isEmpty("abc")).thenReturn(false);
        assertFalse(Utils.isEmpty("abc"));
    }
复制代码

例子2: 测试private方法 替换私有变量

目标方法

 private String name;
 
 private String changeName(String name) {
        return "ABC" + name;
    }
 public String getName() {
        return name;
    }
复制代码

测试方法

  @Test
    public void testPrivate() throws Exception {
        Utils util = new Utils();
        //调用私有方法
        String res = Whitebox.invokeMethod(util, "changeName", "Lily");
        assertEquals("ABCLily",res);
        //替换私有变量  也可以使用MemberModifier来修改
        Whitebox.setInternalState(util,"name","Lily");
        assertEquals("Lily",util.getName());
    }
复制代码

例子3: 测试mock new关键字

目标方法

 public String getPersonName() {
        Person person = new Person("Lily");
        return person.getName();
    }
复制代码

测试方法

 @Test
    public void testNew() throws Exception {
        Person person = PowerMockito.mock(Person.class);
        Utils util = new Utils();
        //当new一个Person对象并传入Lily的时候,返回person
        PowerMockito.whenNew(Person.class).withArguments("Lily").thenReturn(person);
        PowerMockito.when(util.getPersonName()).thenReturn("Diavd");
        assertEquals("Diavd",util.getPersonName());
    }
复制代码

目标方法getPersonName中new了一个Person,直接调用getPersonName方法会报错,所以我们自己创建一个Person,并指定当当new一个Person对象并传入Lily的时候,返回当前创建的person对象。然后在调用getPersonName方法就不会报错了。

Robolectric

前面测试的类和依赖都是原生Java代码,可以直接运行在JVM上,当我们测试Android的时候,需要依赖Android SDK中的android.jar包,android.jar底层没有具体的代码实现,因为它运行在Andorid系统中,Android系统中有默认的实现。

Mockito和PowerMockito都直接运行在JVM上,JVM上没有Android源码相关的实现,那么在做有Adroid相关的依赖的测试的时候,就会报错,这时候就要用到Robolectric啦,当我们去调用android相关的代码的时候,它会拦截并去执行自己对相关代码的实现。

Robolectric官网

添加依赖

testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.robolectric:robolectric:4.3.1'
复制代码

Robolectric 4.0以上需要Android Gradle插件/ Android Studio 3.2或更高版本。

在build.gradle中的android闭包下面添加下面代码,目前版本最高支持andorid sdk 28

android {
    compileSdkVersion 28
    testOptions.unitTests.includeAndroidResources = true
}
复制代码

在gradle.properties文件中添加下面代码

android.enableUnitTestBinaryResources=true
复制代码

第一次运行的时候会下载相关jar包,网速不好可能要等很久

首先创建一个测试类RobolectricTest,添加注解@RunWith(RobolectricTestRunner.class)通知Junit框架该类中的测试方法运行在RobolectricTestRunner中。

@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {...}
复制代码

例子1: 点击button,改变TextView上的文字,判断改变之后的文字是不是预期的

    @Test
    public void clickingButtonShouldChangeMessage() {
        //默认会调用Activity的onCreate()、onStart()、onResume()
//        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
//        TextView textView = activity.findViewById(R.id.tv_text);
//        Button button = activity.findViewById(R.id.btn_click);
//        button.performClick();
//        assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));

        //Robolectric.setupActivity显示过时了,使用ActivityScenario来代替
        //ActivityScenario提供api来启动和驱动Activity的生命周期状态以进行测试,
        // 适用于任意Activity,并能在不同版本的Android上一致工作
        //通过scenario.moveToState来控制生命周期比如  scenario.moveToState(Lifecycle.State.CREATED)
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            TextView textView = activity.findViewById(R.id.tv_text);
            Button button = activity.findViewById(R.id.btn_click);
            button.performClick();
            assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));
        });
    }
复制代码

使用Robolectric.setupActivity可以启动一个Activity,不过使用的时候显示该方法已过期,最新的可以使用ActivityScenario来启动一个Activity

ActivityScenario提供api来启动和驱动Activity的生命周期状态以进行测试,适用于任意Activity,并能在不同版本的Android上一致工作,通过scenario.moveToState来控制生命周期比如 scenario.moveToState(Lifecycle.State.CREATED)

例子2: 点击按钮从MainActivity到UnitTestActivity,Robolectric是运行在JVM上的测试框架,并不会真正的启动UnitTestActivity,但是可以检查MainActivity是不是触发了真正的意图

    //Application用的比较多,可以初始换一个全局的
    private Application context;

    @Before
    public void setUp() throws Exception {
        context = ApplicationProvider.getApplicationContext();
    }
  @Test
    public void testClickButtonToPicking() {
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            Button button = activity.findViewById(R.id.btn_go_to_unit);
            button.performClick();
            //期望的intent
            Intent expectedIntent = new Intent(activity, UnitTestActivity.class);
            //真实的intent
            Intent actual = shadowOf(context)
                    .getNextStartedActivity();
            assertEquals(expectedIntent.getComponent(),actual.getComponent());
        });
    }
复制代码

例子3: Shadow是Robolectric的核心,Robolectric中内置了很多Android SDK中的类的影子,比如ShadowCompoundButton,ShadowTextView,ShadowActivity .....

当一个android.jar中的某个类被调用的时候,Robolectric会尝试寻找该类的影子,调用影子中的方法,通过shadowOf可以很方便的拿到对应类的影子类

测试Toast显示

  @Test
    public void testToast(){
        ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(activity -> {
            Button button = activity.findViewById(R.id.btn_show_toast);
            button.performClick();

            Toast latestToast = ShadowToast.getLatestToast();
            assertNotNull(latestToast);
            assertEquals("测试Toast", ShadowToast.getTextOfLatestToast());
        });
    }
复制代码

更多例子可查看源码 Robolectric

本篇对本地单元测试的一些常用的库做了一些练习,练习完成就算是入门了,之后写单元测试哪里不熟悉就直接去查文档了。而通过本篇练习本篇最主要的收获就是,以后写代码的时候要时刻有测试意识,尽最大努力写出可测试易维护的代码

参考:

Android 官网测试文档

Android单元测试与模拟测试

使用强大的 Mockito 来测试你的代码

Android单元测试(一)

Android单元测试(二)

Mockito与PowerMock的使用基础教程

Mockito教程

一文全面了解Android单元测试

关注下面的标签,发现更多相似文章
评论