阅读 72

单元测试系列之四Mockito

简介

所谓mock就是创建一个类的虚假对象,在测试环境中,替换掉真实对象,以达到以下目的:

  • 验证这个对象的某个方法的调用情况,比如调用了多少次、参数是什么等等;

  • 指定这个对象的某些方法的行为,比如返回特定的值、执行特定的动作等。

要使用Mock,一般需要用到mock框架,本文介绍Mockito这个框架,这个是Java界使用最广泛的一个mock框架。

比如测试用户登陆:

public class LoginPresenter {

   private UserManager userManager = new UserManager();

   public void login(String username, String password) {
       if (username == null || username.length() == 0) return;
       if (password == null || password.length() < 6) return;
       userManager.performLogin(username, password);
  }
 
   public void setUserManager(UserManager userManager) {
       this.userManager = userManager;
  }
}复制代码

对应的测试类:

public class LoginPresenterTest {
   LoginPresenter loginPresenter;

   @Before
   public void setUp() throws Exception {
       loginPresenter = new LoginPresenter();
  }

@Test
   public void testLogin() {
       UserManager mockUserManager = Mockito.mock(UserManager.class);
       //loginPresenter中使用的UserManager必须是Mock出来的对象,只有这样才能验证UserManager.performLogin,
       // 所以需要将mock出来的对象传给loginPresenter
       loginPresenter.setUserManager(mockUserManager);
       loginPresenter.login("aya", "123456");
       //verify传入的必须mock出来的对象,而且必须与loginPresenter中使用的UserManager对象是同一个
       Mockito.verify(mockUserManager).performLogin("aya", "123456");
  }
}复制代码

注意

  1. Mockito.mock()并不是mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,并且返回这个mock对象;而传进去的这个类本身并没有改变,用这个类new出来的对象也没有受到任何改变!

    比如这样写就会出错:

    public class LoginPresenterTest {
    @Test
       public void testLogin() {
           Mockito.mock(UserManager.class);
           UserManager userManager = loginPresenter.getUserManager();
           loginPresenter.login("aya", "123456");
           Mockito.verify(userManager).performLogin("aya", "123456");
      }
    }复制代码
  2. mock出来的对象并不会自动替换掉正式代码里面的对象,你必须要有某种方式把mock对象应用到正式代码里面,上面例子中使用setUserManager()传入了mock的对象。所以下面这个也是个错误的例子:

    public class LoginPresenterTest {
       @Test
       public void testLogin(){
           UserManager mockUserManager = Mockito.mock(UserManager.class); 
           LoginPresenter loginPresenter = new LoginPresenter();
           loginPresenter.login("aya", "123456");
           Mockito.verify(mockUserManager).performLogin("aya", "123456"); 
      }
    }复制代码
  3. 使用setter的方式传入mock对象时,如果该setter方法其他地方没用到,只是为了测试而增加的,不是很优雅。可以使用依赖注入的方式来解决,比如把UserManager作为LoginPresenter的构造函数的参数,最终如下:

    public class LoginPresenter {
    
       private UserManager mUserManager;
    
       public LoginPresenter(UserManager userManager){
           mUserManager = userManager;
      }
       
       public void login(String username, String password) {
           if (username == null || username.length() == 0) return;
           if (password == null || password.length() < 6) return;
           mUserManager.performLogin(username, password);
      }
    }
    
    public class LoginPresenterTest {
       LoginPresenter loginPresenter;
       @Test
       public void testLogin() {
           UserManager mockUserManager = Mockito.mock(UserManager.class);
           //loginPresenter中使用的UserManager必须是Mock出来的对象,只有这样才能验证UserManager.performLogin,
           // 所以需要将mock出来的对象传给loginPresenter
           loginPresenter = new LoginPresenter(mockUserManager);
           loginPresenter.login("aya", "123456");
           //verify传入的必须mock出来的对象,而且必须与loginPresenter中使用的UserManager对象是同一个
           Mockito.verify(mockUserManager).performLogin("aya", "123456");
      }
    }复制代码

验证方法调用

查看Mockito源码,可以看到verify()还有一个重载的方法:

   /**
    * Verifies certain behavior happened at least once / exact number of times / never. E.g:
    * <pre class="code"><code class="java">
    *   verify(mock, times(5)).someMethod("was called five times");
    *
    *   verify(mock, atLeast(2)).someMethod("was called at least two times");
    *
    *   //you can use flexible argument matchers, e.g:
    *   verify(mock, atLeastOnce()).someMethod(<b>anyString()</b>);
    * </code></pre>
    *
    * <b>times(1) is the default</b> and can be omitted
    * <p>
    * Arguments passed are compared using <code>equals()</code> method.
    * Read about {@link ArgumentCaptor} or {@link ArgumentMatcher} to find out other ways of matching / asserting arguments passed.
    * <p>
    *
    * @param mock to be verified
    * @param mode times(x), atLeastOnce() or never()
    *
    * @return mock object itself
    */
   @CheckReturnValue
   public static <T> T verify(T mock, VerificationMode mode) {
       return MOCKITO_CORE.verify(mock, mode);
  }复制代码

其中VerificationMode源码如下:

/**
* Allows verifying that certain behavior happened at least once / exact number
* of times / never. E.g:
*
* <pre class="code"><code class="java">
* verify(mock, times(5)).someMethod(&quot;was called five times&quot;);
*
* verify(mock, never()).someMethod(&quot;was never called&quot;);
*
* verify(mock, atLeastOnce()).someMethod(&quot;was called at least once&quot;);
*
* verify(mock, atLeast(2)).someMethod(&quot;was called at least twice&quot;);
*
* verify(mock, atMost(3)).someMethod(&quot;was called at most 3 times&quot;);
*
* </code></pre>
*
* <b>times(1) is the default</b> and can be omitted
* <p>
* See examples in javadoc for {@link Mockito#verify(Object, VerificationMode)}
*/
public interface VerificationMode {

   /**
    * Performs the verification
    */
   void verify(VerificationData data);

   /**
    * Description will be prepended to the assertion error if verification fails.
    * @param description The custom failure message
    * @return VerificationMode
    * @since 2.1.0
    */
   VerificationMode description(String description);
}复制代码

可以看出,在验证方法调用时,可以使用times()指定调用次数,使用never()验证从未调用,使用 atLeastOnce()验证至少调用了一次,使用atLeast()验证至少调用次数,使用atMost()验证最多调用次数。

如果你不关心传入的参数,只关心方法是否调用,Mockito也提供了一系列的any方法,来表示任何的参数都行:

Mockito.verify(mockUserManager).performLogin(Mockito.anyString(), Mockito.anyString());复制代码

Mockito.anyString()表示任意非null字符串,类似的有anyFloat(),anyBoolean,anyByte(),anyLong(),也提供了anyList,anyListOf(Class<T> clazz),anySet(),anyMap(),anyCollection等方法,甚至isNotNull(),isNotNull(Class<T> clazz),anyObject,更多方法去看源码。

指定返回值

假设上面的LoginPresenter的login()方法是这么实现的:

public void login(String username, String password) {
       if (username == null || username.length() == 0) return;
       mPasswordValidator = new PasswordValidator();
       if (!mPasswordValidator.verifyPassword(password)) return;
       mUserManager.performLogin(username, password);
  }复制代码

在登陆时,需要验证密码的正确性,而此时可能需要联网验证,所以会很耗时。在测试时就可以简单处理,比如直接返回true或者false。因为测试的是login()方法,和验证密码的内部逻辑没有关系,所以可以这么做,这才是单元测试该有的粒度。

指定mock对象的某个方法,让它返回特定值的写法如下:

Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue);复制代码

所以我们可以这样写:

//先创建一个mock对象
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);
//当调用mockValidator的verifyPassword方法,同时传入"123"时,返回true
Mockito.when(mockValidator.verifyPassword("123")).thenReturn(true);
//当调用mockValidator的verifyPassword方法,同时传入"123456"时,返回false
Mockito.when(validator.verifyPassword("123456")).thenReturn(false);复制代码

由于已经指定了返回值,所以并不会执行verifyPassword()方法的真实逻辑!

此时,测试类可以这样写:

public class LoginPresenterTest {
   LoginPresenter loginPresenter;
   @Test
   public void testLogin() {
       UserManager mockUserManager = Mockito.mock(UserManager.class);
       PasswordValidator mPasswordValidator = Mockito.mock(PasswordValidator.class);
       Mockito.when(mPasswordValidator.verifyPassword(Mockito.anyString())).thenReturn(true);//当thenReturn(false)时,测试失败,因为mUserManager.performLogin(username, password)代码不会执行

       loginPresenter = new LoginPresenter(mockUserManager, mPasswordValidator);
       loginPresenter.login("aya", "123");
       Mockito.verify(mockUserManager, Mockito.times(1)).performLogin("aya", "123");
  }
}复制代码

注意:mock出来的对象mPasswordValidator一定要传进去!

执行特定动作

假设我们的LoginPresenterlogin()方法是这样的:

public void login(String username, String password) {
       if (username == null || username.length() == 0) return;
       if (!mPasswordValidator.verifyPassword(password)) return;
       mUserManager.performLogin(username, password, new UserManager.NetCallback() {
           @Override
           public void onSuccess(Object data) {
               //登陆成功,用数据更新UI

          }

           @Override
           public void onFailure(String msg) {
               //登陆失败,显示msg

          }
      });
  }复制代码

在这里,我们想进一步测试传给mUserManager.performLoginNetCallback里面的代码,验证view得到了更新等等。在测试环境下,我们并不想依赖mUserManager.performLogin的真实逻辑,而是让mUserManager直接调用传入的NetCallbackonSuccessonFailure方法。这种指定mock对象执行特定的动作的写法如下: Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args); 传给doAnswer()的是一个Answer对象,我们想要执行什么样的动作,就在这里面实现。结合上面的例子解释:

public class LoginPresenterTest {
   LoginPresenter loginPresenter;
   @Test
   public void testLogin() {
       UserManager mockUserManager = Mockito.mock(UserManager.class);
       PasswordValidator mPasswordValidator = Mockito.mock(PasswordValidator.class);
       Mockito.when(mPasswordValidator.verifyPassword(Mockito.anyString())).thenReturn(true);

       loginPresenter = new LoginPresenter(mockUserManager, mPasswordValidator);

       Mockito.doAnswer(new Answer() {
           @Override
           public Object answer(InvocationOnMock invocation) throws Throwable {
               //这里可以获得传给performLogin的参数
               Object[] arguments = invocation.getArguments();

               //callback是第三个参数
               UserManager.NetCallback callback = (UserManager.NetCallback) arguments[2];
               callback.onFailure("404 Not found");
               return 404;
          }
      }).when(mockUserManager).performLogin(Mockito.anyString(), Mockito.anyString(), Mockito.any(UserManager.NetCallback.class));

       loginPresenter.login("aya", "123456");
  }
}复制代码

当调用mockUserManagerperformLogin方法时,会执行answer里面的代码,我们上面的例子是直接调用传入的callbackonFailure方法,同时传给onFailure方法404和"Not found"。

除了doAnswer(),mockito还提供了doNothing(),用来指定目标方法“什么都不做”;doThrow(desiredException),指定目标方法“抛出一个异常”;doCallRealMethod(),让目标方法调用真实的逻辑。

Spy

对于一个mock对象,我们可以指定返回值和执行特定的动作,当然,也可以不指定,如果不指定的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。

如果你想实现这样的效果:指定时执行指定的动作,不指定时调用这个对象的默认实现,同时又能拥有验证方法调用的功能。那你可以使用Mockito.spy()来创建对象。

创建一个spy对象,以及spy对象的用法介绍如下:

public class PasswordValidator {
   public boolean verifyPassword(String password) {
       if (password == null || password.length() < 6) {
           return false;
      }
       return true;
  }
}

public class PasswordValidatorTest {
   @Test
   public void testSpy(){
       PasswordValidator mockPasswordValidator = Mockito.mock(PasswordValidator.class);
       boolean b0 = mockPasswordValidator.verifyPassword("1234567");//不指定时mock对象返回默认值
       System.out.println("测试mock默认行为: " + b0);

       Mockito.when(mockPasswordValidator.verifyPassword("1234567")).thenReturn(true);//mock对象指定行为:返回true
       boolean b1 = mockPasswordValidator.verifyPassword("1234567");//指定时mock对象返回指定值
       System.out.println("测试mock指定行为: " + b1);

       PasswordValidator spyPasswordValidator = Mockito.spy(PasswordValidator.class);
       boolean b2 = spyPasswordValidator.verifyPassword("1234567");//不指定时spy对象调用真实逻辑
       System.out.println("测试spy默认行为: " + b2);

       Mockito.when(spyPasswordValidator.verifyPassword("1234567")).thenReturn(false);//spy对象指定行为:返回false
       boolean b3 = spyPasswordValidator.verifyPassword("1234567");//指定时spy对象返回指定值
       System.out.println("测试spy指定行为: " + b3);
     
       Mockito.verify(spyPasswordValidator).verifyPassword("1234567");//测试PasswordValidator.verifyPassword("1234567")调用次数,结果失败,因为上面已经调用了两次passwordValidator.verifyPassword("1234567")

  }
}复制代码

运行测试方法testSpy(),输出结果如下:

截屏2021-09-03 上午10.53.23.png

通过对比可以看出:spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。

至此,有关mockito的用法介绍完毕,下一篇介绍powermock。


作者:大师傅姑爷
链接:https://juejin.cn/post/7019598783696601101


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐