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

栏目: IT技术 · 发布时间: 4年前

内容简介:Android自动化测试入门(四)单元测试单元测试一般分两类:本地单元测试在Android自动化测试中是比重最大的一环,主要针对某个类中的某个方法。谷歌建议在所有的测试中,单元测试要占到70%的比重,为啥它就这么重要呢?

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单元测试


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

技术之瞳——阿里巴巴技术笔试心得

技术之瞳——阿里巴巴技术笔试心得

阿里巴巴集团校园招聘笔试项目组 / 电子工业出版社 / 2016-11 / 69

《技术之瞳——阿里巴巴技术笔试心得》由阿里巴巴集团校园招聘笔试项目组所著,收集了阿里历年校招中的精华笔试题,涉 及多个领域。《技术之瞳——阿里巴巴技术笔试心得》中内容大量结合了阿里巴巴的实际工作场景,以例题、解析、习题的形式,引 导读者深入理解技术上的关键点、紧要处,夯实基础,启发思考。《技术之瞳——阿里巴巴技术笔试心得》内容不仅专业、有趣,更 是将理论知识与实践应用结合起来,以场景化的问答娓娓道......一起来看看 《技术之瞳——阿里巴巴技术笔试心得》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具