Android单元测试之Mockito测试框架的使用(一)

发表于:2021-1-08 10:11

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:chenxibobo    来源:博客园

  Mockito测试框架的使用
  前面介绍的只能测试不涉及Android相关Api的java代码用例,如果涉及到Android相关Api的时候,就不方便了,这时如果不依赖第三方库的话可能需要使用仪器化测试跑到Android设备上去运行,于是有一些比较好的第三方的替代框架可以来模拟使用Android的代码测试,Mockito就是基于依赖注入实现的一个测试框架。
  1. Mock概念的理解
  什么是Mock, 这个单词的中文意思就是“模仿”或者“虚假”的意思,也就是要模仿一个对象,为啥要模仿?
  在传统的JUnit单元测试中,没有消除在测试中对对象的依赖,如A对象依赖B对象方法,在测试A对象的时候,我们需要构造出B对象,这样子增加了测试的难度,或者使得我们对某些类的测试无法实现。这与单元测试的思路相违背。
  还有一个主要的问题就是本地单元测试由于是运行本地JVM环境,无法依赖Android的api,只靠纯Junit的测试环境很难模拟出完整的Android环境,导致无法测试Android相关的代码,而Mock就能解决这个问题,通过Mock能够很轻易的实现对象的模拟。
  添加依赖:
  dependencies {
      implementation fileTree(dir: 'libs', include: ['*.jar'])
      testImplementation 'org.mockito:mockito-core:2.19.0'
      ....
  }
  2. Mockito中几种Mock对象的方式
  使用之前通过静态方式导入会使用更方便:
   // 静态导入会使代码更简洁
   import static org.mockito.Mockito.*;
  直接mock一个对象:
      @Test
      public void testMock() {
          SimpleClass mockSimple = Mockito.mock(SimpleClass.class);
          assertNotNull(mockSimple);
      }
  注解方式mock一个对象:
      @Mock
      SimpleClass simple;
      @Before
      public void setUp() {
          MockitoAnnotations.initMocks(this);
      }
      @Test
      public void testMock() {
          assertNotNull(simple);
      }
  运行器方式mock一个对象:
  @RunWith(MockitoJUnitRunner.class)
  public class ExampleUnitTest {
      @Mock
      SimpleClass simple;
      
      @Test
      public void testMock() {
          assertNotNull(simple);
      }
  }
  MockitoRule方式mock一个对象:
  public class ExampleUnitTest {
      @Mock
      SimpleClass simple;
      
      @Rule //<--使用@Rule
      public MockitoRule mockitoRule = MockitoJUnit.rule();
      
      @Test
      public void testMock() {
          assertNotNull(simple);
      }
  }
  3. 验证行为
  ·verify(T mock)函数的使用
  verify(T mock)的作用是验证发生的某些行为等同于verify(mock, times(1)) 例如:
  @Test
  public void testMock() {
   //创建mock对象
   List mockedList = mock(List.class);
   //使用mock对象
   mockedList.add("one");
   mockedList.clear();
  
   //验证mockedList.add("one")是否被调用,如果被调用则当前测试方法通过,否则失败
   verify(mockedList).add("one");
   //验证 mockedList.clear()是否被调用,如果被调用则当前测试方法通过,否则失败
   verify(mockedList).clear();
   }

  @Test
  public void testMock() {
  mock.someMethod("some arg");
  //验证mock.someMethod("some arg")是否被调用,如果被调用则测试方法通过,否则失败
  verify(mock).someMethod("some arg");
  
  }
  也就是说如果把调用的方法注释掉,则运行testMock()方法就会失败。
  通过verify关键字,一旦mock对象被创建了,mock对象会记住所有的交互。然后你就可能选择性的验证你感兴趣的交互。
  通常需要配合一些测试方法来验证某些行为,这些方法称为"打桩方法"(Stub),打桩的意思是针对mock出来的对象进行一些模拟操作,如设置模拟的返回值或抛出异常等。
  常见的打桩方法:
  例如:
   @Test
   public void testMock() {
   // 你可以mock具体的类型,不仅只是接口
   List mockedList = mock(List.class);
   // 打测试桩
   when(mockedList.get(0)).thenReturn("first");
   doReturn("aaaa").when(mockedList).get(1);
   when(mockedList.get(1)).thenThrow(new RuntimeException());
   doThrow(new RuntimeException()).when(mockedList).clear();
   // 输出“first”
   System.out.println(mockedList.get(0));
   // 因为get(999) 没有打桩,因此输出null, 注意模拟环境下这个地方是不会报IndexOutOfBoundsException异常的
   System.out.println(mockedList.get(999));
   // get(1)时会抛出异常
   System.out.println(mockedList.get(1));
   // clear会抛出异常
   mockedList.clear();
   }
  doXXX和thenXXX使用上差不多,一个是调用方法之前设置好返回值,一个是在调用方法之后设置返回值。默认情况下,Mock出的对象的所有非void函数都有返回值,对象类型的默认返回的是null,例如返回int、boolean、String的函数,默认返回值分别是0、false和null。
  ·使用when(T methodCall)函数
  打桩方法需要配合when(T methodCall)函数,意思是使测试桩方法生效。当你想让这个mock能调用特定的方法返回特定的值,那么你就可以使用它。
  例如:
  when(mock.someMethod()).thenReturn(10);
   //你可以使用灵活的参数匹配,例如 
   when(mock.someMethod(anyString())).thenReturn(10);
   //设置抛出的异常
   when(mock.someMethod("some arg")).thenThrow(new RuntimeException());
   //你可以对不同作用的连续回调的方法打测试桩:
   //最后面的测试桩(例如:返回一个对象:"foo")决定了接下来的回调方法以及它的行为。
   
   when(mock.someMethod("some arg"))
    .thenReturn("foo")//第一次调用someMethod("some arg")会返回"foo"
    .thenThrow(new RuntimeException());//第二次调用someMethod("some arg")会抛异常
    
   //可以用以下方式替代比较小版本的连贯测试桩:
   when(mock.someMethod("some arg"))
    .thenReturn("one", "two");
   //和下面的方式效果是一样的
   when(mock.someMethod("some arg"))
    .thenReturn("one")
    .thenReturn("two");
   //比较小版本的连贯测试桩并且抛出异常:
   when(mock.someMethod("some arg"))
    .thenThrow(new RuntimeException(), new NullPointerException();
  ·使用thenAnswer为回调做测试桩
  when(mock.someMethod(anyString())).thenAnswer(new Answer() {
       Object answer(InvocationOnMock invocation) {
           Object[] args = invocation.getArguments();
           Object mock = invocation.getMock();
           return "called with arguments: " + args;
       }
   });
   // 输出 : "called with arguments: foo"
   System.out.println(mock.someMethod("foo"));
  ·使用doCallRealMethod()函数来调用某个方法的真实实现方法
  注意,在Mock环境下,所有的对象都是模拟出来的,而方法的结果也是需要模拟出来的,如果你没有为mock出的对象设置模拟结果,则会返回默认值,例如:
  public class Person {
      public String getName() {
          return "小明";
      }
  }
  @Test
  public void testPerson() {
      Person mock = mock(Person.class);
      //输出null,除非设置发回模拟值when(mock.getName()).thenReturn("xxx");
      System.out.println(mock.getName());
  }
  因为getName()方法没有设置模拟返回值,而getName()返回值是String类型的,因此直接调用的话会返回String的默认值null,所以上面代码如果要想输出getName()方法的真实返回值的话,需要设置doCallRealMethod():
   @Test
   public void testPerson() {
       Person mock = mock(Person.class);
       doCallRealMethod().when(mock).getName();
       //输出“小明”
       System.out.println(mock.getName());
   }
  ·使用doNothing()函数是为了设置void函数什么也不做
  需要注意的是默认情况下返回值为void的函数在mocks中是什么也不做的但是,也会有一些特殊情况。如:
  测试桩连续调用一个void函数时:
     doNothing().doThrow(new RuntimeException()).when(mock).someVoidMethod();
     //does nothing the first time:
     mock.someVoidMethod();
     //throws RuntimeException the next time:
     mock.someVoidMethod();
  监控真实的对象并且你想让void函数什么也不做:
  List list = new LinkedList();
  List spy = spy(list);
  //let's make clear() do nothing
  doNothing().when(spy).clear();
  spy.add("one");
  //clear() does nothing, so the list still contains "one"
  spy.clear();
  ·使用doAnswer()函数测试void函数的回调
  当你想要测试一个无返回值的函数时,可以使用一个含有泛型类Answer参数的doAnswer()函数做回调测试。假设你有一个void方法有多个回调参数,当你想指定执行某个回调时,使用thenAnswer很难实现了,如果使用doAnswer()将非常简单,示例代码如下: 
  MyCallback callback = mock(MyCallback.class);
  Mockito.doAnswer(new Answer() {
      @Override
      public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
          //获取第一个参数
          MyCallback call = invocation.getArgument(0);
          //指定回调执行操作
          call.onSuccess();
          return null;
      }
  }).when(mockedObject.requset(callback));
  doAnswer(new Answer() {
           @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                System.out.println("onSuccess answer");
                return null;
            }
   }).when(callback).onSuccess();
   
  mockedObject.requset(callback)
  ·需要使用doReturn函数代替thenReturn的情况
  如当监控真实的对象并且调用真实的函数带来的影响时
  List list = new LinkedList();
  List spy = spy(list);
  //不可能完成的:真实方法被调用的时候list仍是空的,所以spy.get(0)会抛出IndexOutOfBoundsException()异常
  when(spy.get(0)).thenReturn("foo");
  //这时你应该使用doReturn()函数
  doReturn("foo").when(spy).get(0);
  ·使用doThrow()函数来测试void函数抛出异常
  SimpleClass mock = mock(SimpleClass.class);
  doThrow(new RuntimeException()).when(mock).someVoidMethod();
  mock.someVoidMethod();
  总之使用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 这些函数时可以在适当的情况下调用when()来解决一些问题., 如当你需要下面这些功能时这是必须的:
  ·测试void函数 
  ·在受监控的对象上测试函数 
  ·不只一次的测试同一个函数,在测试过程中改变mock对象的行为
  4. 验证方法的调用次数
  需要配合使用一些方法:
  例如:
  mock.someMethod("some arg");
  mock.someMethod("some arg");
  //验证mock.someMethod("some arg")被连续调用两次,即如果没有调用两次则验证失败
  verify(mock, times(2)).someMethod("some arg");

  //注意,下面三种是等价的,都是验证someMethod()被只调用一次
  verify(mock).someMethod("some arg");
  verify(mock, times(1)).someMethod("some arg");
  verify(mock, only()).someMethod("some arg");

  mPerson.getAge();
  mPerson.getAge();
  //验证至少调用2次
  verify(mPerson, atLeast(2)).getAge();
  //验证至多调用2次
  verify(mPerson, atMost(2)).getAge();

  //下面两种等价,验证调用次数为0
  verify(mPerson, never()).getAge();
  verify(mPerson, times(0)).getAge();

  mPerson.getAge();
  mPerson.getAge();
  long current = System.currentTimeMillis();
  System.out.println(current );
  //延时1s后验证mPerson.getAge()是否被执行了2次
  verify(mPerson, after(1000).times(2)).getAge();
  System.out.println(System.currentTimeMillis() - current);

   mPerson.getAge();
   mPerson.getAge();
   //验证方法在100ms超时前被调用2次
   verify(mPerson, timeout(100).times(2)).getAge();

    @Test
    public void testVerifyZeroInteractions() {
        Person person = mock(Person.class);
        person.eat("a");
    //由于person对象发生了交互,所以这里验证失败,把上面的调用注释掉这里就会验证成功
        verifyZeroInteractions(person);
        //可以验证多个对象没有交互
        //verifyZeroInteractions(person,person2 );
    }

    @Test
    public void testVerifyZeroInteractions() {
        Person person = mock(Person.class);
        person.eat("a");
        verify(person).eat("a");
    //注意,这将会无法到达验证目的,不能跟verify()混用
        verifyZeroInteractions(person,person2 );
    }

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号