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


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

查看所有标签

猜你喜欢:

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

The Web Application Hacker's Handbook

The Web Application Hacker's Handbook

Dafydd Stuttard、Marcus Pinto / Wiley / 2011-9-27 / USD 50.00

The highly successful security book returns with a new edition, completely updated Web applications are the front door to most organizations, exposing them to attacks that may disclose personal infor......一起来看看 《The Web Application Hacker's Handbook》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具