单元测试系列之四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"); } }复制代码
注意
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"); } }复制代码
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"); } }复制代码
使用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("was called five times"); * * verify(mock, never()).someMethod("was never called"); * * verify(mock, atLeastOnce()).someMethod("was called at least once"); * * verify(mock, atLeast(2)).someMethod("was called at least twice"); * * verify(mock, atMost(3)).someMethod("was called at most 3 times"); * * </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
一定要传进去!
执行特定动作
假设我们的LoginPresenter
的login()
方法是这样的:
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.performLogin
的NetCallback
里面的代码,验证view得到了更新等等。在测试环境下,我们并不想依赖mUserManager.performLogin
的真实逻辑,而是让mUserManager
直接调用传入的NetCallback
的onSuccess
或onFailure
方法。这种指定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"); } }复制代码
当调用mockUserManager
的performLogin
方法时,会执行answer里面的代码,我们上面的例子是直接调用传入的callback
的onFailure
方法,同时传给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()
,输出结果如下:
通过对比可以看出:spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。
至此,有关mockito的用法介绍完毕,下一篇介绍powermock。
作者:大师傅姑爷
链接:https://juejin.cn/post/7019598783696601101