http://www.open-open.com/lib/view/open1462177583813.html [From] http://www.open-open.com/lib/view/open1462177583813.htmlhtml
几点说明:java
代码中的 //<== 表示跟上面的相比,这是新增的,或者是修改的代码,不知道怎么样在代码块里面再强调几行代码T_T。。。android
不少时候,为了不中文歧义,我会用英文表述git
在第一篇文章里面 咱们提到,返回类型为void方法的单元测试方式,每每是验证里面的某个对象的某个方法是否获得了调用。在那篇文章里面,我举的例子是activity里面的一个login方法:github
public void login() {
String username = ...//get username from username EditText
String password = ...//get password from password EditText
//do other operation like validation, etc
...
mUserManager.performLogin(username, password);
}
对于这个login方法的单元测试,应该是调用Activity里面的这个login方法,而后验证 mUserManager 的 performLogin 方法获得了验证。可是若是使用Activity,咱们就须要用到 Robolectric 框架,然而咱们到目前为止尚未讲到Robolectric的使用。因此在这篇文章中,咱们假设这段代码是放在一个Presenter(LoginPresenter)里面的,这个是 MVP模式 里面的概念,这个 LoginPresenter 是一个纯java类,而用户名和密码是外面传进来的:框架
public class LoginPresenter {
private UserManager mUserManager = new 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);
}
}
根据 前面一篇关于JUnit的文章 的讲解,咱们很容易的写出针对 login() 方法的单元测试:ide
public class LoginPresenterTest {
@Test
public void testLogin() throws Exception {
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.login("xiaochuang", "xiaochuang password");
//验证LoginPresenter里面的mUserManager的performLogin()方法获得了调用,同时参数分别是“xiaochuang”、“xiaochuang‘s password”
...
}
}
如今,关键的问题来了,怎么验证 LoginPresenter 里面的 mUserManager 的 performLogin() 方法获得了调用,以及它的参数是正确性呢?若是你们看了 该系列的第一篇文章 就知道,这里须要用到 mock ,那么接下来,咱们就介绍mock这个东西。函数
Mock的概念,其实很简单,咱们前面也介绍过:所谓的mock就是建立一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:单元测试
验证这个对象的某些方法的调用状况,调用了多少次,参数是什么等等测试
指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动做
要使用Mock,通常须要用到mock框架,这篇文章咱们使用 Mockito 这个框架,这个是Java界使用最普遍的一个mock框架。
对于上面的例子,咱们要验证 mUserManager 的一些行为,首先要mock UserManager这个类,mock这个类的方式是:
Mockito.mock(UserManager.class); mock了 UserManager 类以后,咱们就能够开始测试了:
public class LoginPresenterTest {
@Test
public void testLogin() {
Mockito.mock(UserManager.class); //<==
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.login("xiaochuang", "xiaochuang password");
//验证LoginPresenter里面的mUserManager的performLogin()方法获得了调用,参数分别是“xiaochuang”、“xiaochuang‘s password”
...
}
}
然而咱们要验证的是 LoginPresenter 里面的 mUserManager 这个对象,可是如今咱们没有办法得到这个对象,由于 mUserManager 是private的,怎么办?先不想太多,咱们简单除暴点,给 LoginPresenter 加一个getter,稍后你会明白我如今为何作这样的决定。
public class LoginPresenter {
private UserManager mUserManager = new 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 UserManager getUserManager() { //<==
return mUserManager;
}
}
好了,如今咱们能够验证 mUserManager 被调用的状况了:
public class LoginPresenterTest {
@Test
public void testLogin() throws Exception {
Mockito.mock(UserManager.class);
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.login("xiaochuang", "xiaochuang password");
UserManager userManager = loginPresenter.getUserManager(); //<==
//验证userManager的performLogin()方法获得了调用,参数分别是“xiaochuang”、“xiaochuang password”
...
}
}
终于到了解释如何验证一个对象的某个方法的调用状况了。使用Mockito,验证一个对象的方法调用状况的姿式是:
Mockito.verify(objectToVerify).methodToVerify(arguments);
其中, objectToVerify 和 methodToVerify 分别是你想要验证的对象和方法。对应上面的例子,那就是:
Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password"); 好,如今咱们把这行代码放到测试里面:
public class LoginPresenterTest {
@Test
public void testLogin() throws Exception {
Mockito.mock(UserManager.class);
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.login("xiaochuang", "xiaochuang password");
UserManager userManager = loginPresenter.getUserManager();
Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password"); //<==
}
}
接着咱们跑一下这个测试方法,结果发现,额。。。出错了:
具体出错的是最后这一行代码: Mockito.verify(userManager).performLogin("xiaochuang", "xiaochuang password"); 。这个错误的大概意思是,传给 Mockito.verify() 的参数必须是一个mock对象,而咱们传进去的不是一个mock对象,因此出错了。
这就是我想解释的,关于mock的第一个误解: Mockito.mock() 并非mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,而且返回这个mock对象;而传进去的这个类自己并无改变,用这个类new出来的对象也没有受到任何改变!
结合上面的例子, Mockito.mock(UserManager.class); 只是返回了一个属于 UserManager 这个类的一个mock对象。 UserManager 这个类自己没有受到任何影响,而 LoginPresenter 里面直接 new UserManager() 获得的 mUserManager 也是正常的一个对象,不是一个mock对象。 Mockito.verify() 的参数必须是mock对象,也就是说,Mockito只能验证mock对象的方法调用状况。所以,上面那种写法就出错了。
好的,知道了,既然这样,看来咱们须要使用 Mockito.mock(UserManager.class); 返回的对象来验证,代码以下:
public class LoginPresenterTest {
@Test
public void testLogin() throws Exception {
UserManager mockUserManager = Mockito.mock(UserManager.class); //<==
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.login("xiaochuang", "xiaochuang password");
Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password"); //<==
}
}
在运行一下,发现,额。。。又出错了:
错误信息的大意是,咱们想验证 mockUserManager 的 performLogin() 方法获得了调用,然而其实并无。
这就是我想解释的,关于mock的第二个误解: mock出来的对象并不会自动替换掉正式代码里面的对象,你必需要有某种方式把mock对象应用到正式代码里面
结合上面的例子, UserManager mockUserManager = Mockito.mock(UserManager.class); 的确给咱们建立了一个mock对象,保存在 mockUserManager 里面。然而,当咱们调用 loginPresenter.login("xiaochuang", "xiaochuang password"); 的时候,用到的mUserManager依然是使用 new UserManager() 建立的正常的对象。而 mockUserManager 并无获得任何的调用,所以,当咱们验证它的 performLogin() 方法获得了调用时,就失败了。
对于这个问题,很明显,咱们必须在调用 loginPresenter.login() 以前,把 mUserManager 引用换成 mockUserManager 所引用的mock对象。最简单的办法,就是加一个setter:
public class LoginPresenter {
private UserManager mUserManager = new 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 void setUserManager(UserManager userManager) { //<==
this.mUserManager = userManager;
}
}
同时,getter咱们用不到了,因而这里就直接删了。那么按照上面的思路,写出来的测试代码以下:
@Test
public void testLogin() throws Exception {
UserManager mockUserManager = Mockito.mock(UserManager.class);
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.setUserManager(mockUserManager); //<==
loginPresenter.login("xiaochuang", "xiaochuang password");
Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
}
最后运行一次,hu。。。终于经过了!
固然,若是你的正式代码里面没有任何地方用到了那个setter的话,那么专门为了测试而增长了一个方法,毕竟不是很优雅的解决办法,更好的解决办法是使用依赖注入,简单解释就是把 UserManager 做为 LoginPresenter 的构造函数的参数,传进去。具体操做请期待下一篇文章^_^,这里咱们专门讲mock的概念和Mockito的使用。
然而仍是忍不住想多嘴一句:优雅归优雅,有么有必要,值不值得,却又是另一回事。整体来讲,我认为是值得的,由于这可让这个类变得可测,也就意味着咱们能够验证这个类的正确性,更给之后重构这个类有了保障,防止误改错这个类等等。所以,不少时候,若是你为了作单元测试,不得已要给一些类加一些额外的代码。那就加吧!毕竟优雅不能当饭吃,而解决问题、修复bug能够,作出优秀的、少有bug的产品更能够,因此,Just Do It!
好了,如今我想你们对mock的概念应该有了正确的认识,对怎么样使用mock也有了认识,接下来咱们就能够全心全意介绍Mockito的功能和使用了。
1. 验证方法调用
前面咱们讲了验证一个对象的某个method获得调用的方法:
Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
这句话的做用是,验证 mockUserManager 的 performLogin() 获得了调用,同时参数是“xiaochuang”和"xiaochuang password"。其实更准确的说法是,这行代码验证的是, mockUserManager 的 performLogin() 方法获得了 一次 调用。由于这行代码实际上是:
Mockito.verify(mockUserManager, Mockito.times(1)).performLogin("xiaochuang", "xiaochuang password");
的简写,或者说重载方法,注意其中的 Mockito.times(1) 。
所以,若是你想验证一个对象的某个方法获得了屡次调用,只须要将次数传给 Mockito.times() 就行了。
Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...); //验证mockUserManager的performLogin获得了三次调用。
对于调用次数的验证,除了能够验证固定的多少次,还能够验证最多,最少历来没有等等,方法分别是: atMost(count), atLeast(count), never() 等等,都是Mockito的静态方法,其实大部分时候咱们会static import Mockito这个类的全部静态方法,这样就不用每次加上 Mockito. 前缀了。本文下面我也按照这个规则。(其实我早就想说这句话啦,只是一直没找到好的时机[喜极而泣])
不少时候你并不关心被调用方法的参数具体是什么,或者是你也不知道,你只关心这个方法获得调用了就行。这种状况下,Mockito提供了一系列的 any 方法,来表示任何的参数都行:
Mockito.verify(mockUserManager).performLogin(Mockito.anyString(), Mockito.anyString());
anyString() 表示任何一个字符串均可以。null?也能够的!
相似 anyString ,还有 anyInt, anyLong, anyDouble 等等。 anyObject 表示任何对象, any(clazz) 表示任何属于clazz的对象。在写这篇文章的时候,我刚刚发现,还有很是有意思也很是人性化的 anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz) 等等。看来我以前写了很多冤枉代码啊T_T。。。
2. 指定mock对象的某些方法的行为
到目前为止,咱们介绍了mock的一大做用:验证方法调用。咱们说mock主要有两大做用,第二个大做用是:指定某个方法的返回值,或者是执行特定的动做。
那么接下来,咱们就来介绍mock的第二大做用,先介绍其中的第一点:指定mock对象的某个方法返回特定的值。
如今假设咱们上面的 LoginPresenter 的 login 方法是以下实现的:
public void login(String username, String password) {
if (username == null || username.length() == 0) return;
//假设咱们对密码强度有必定要求,使用一个专门的validator来验证密码的有效性
if (mPasswordValidator.verifyPassword(password)) return; //<==
mUserManager.performLogin(null, password);
}
这里,咱们有个 PasswordValidator 来验证密码的有效性,可是这个类的 verifyPassword() 方法运行须要好久,好比说须要联网。这个时候在测试的环境下咱们想简单处理,指定让它直接返回true或false。你可能会想,这样作能够吗?真的好吗?回答是确定的,由于这里咱们要测的是 login() 这个方法,这其实跟 PasswordValidator 内部的逻辑没有太大关系,这才是单元测试真正该有的粒度。
话说回来,这种指定mock对象的某个方法,让它返回特定值的写法以下:
Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue); 应该很好理解,结合上面PasswordValidator的例子:
//先建立一个mock对象
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_handsome"时,返回true
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_not_handsome"时,返回false
Mockito.when(validator.verifyPassword("xiaochuang_is_not_handsome")).thenReturn(false);
一样的,你能够用 any 系列方法来指定"不管传入任何参数值,都返回xxx":
//当调用mockValidator的verifyPassword方法时,返回true,不管参数是什么
Mockito.when(validator.verifyPassword(anyString())).thenReturn(true);
指定方法返回特定值就介绍到这,更详细更高级的用法你们能够本身google。接下来介绍,怎么样指定一个方法执行特定的动做,这个功能通常是用在目标的方法是void类型的时候。
如今假设咱们的 LoginPresenter 的 login() 方法是这样的:
public void loginCallbackVersion(String username, String password) {
if (username == null || username.length() == 0) return;
//假设咱们对密码强度有必定要求,使用一个专门的validator来验证密码的有效性
if (mPasswordValidator.verifyPassword(password)) return;
//login的结果将经过callback传递回来。
mUserManager.performLogin(username, password, new NetworkCallback() { //<==
@Override
public void onSuccess(Object data) {
//update view with data
}
@Override
public void onFailure(int code, String msg) {
//show error msg
}
});
}
在这里,咱们想进一步测试传给 mUserManager.performLogin 的 NetworkCallback 里面的代码,验证view获得了更新等等。在测试环境下,咱们并不想依赖 mUserManager.performLogin 的真实逻辑,而是让 mUserManager 直接调用传入的 NetworkCallback 的 onSuccess 或 onFailure 方法。这种指定mock对象执行特定的动做的写法以下:
Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args); 传给 doAnswer() 的是一个 Answer 对象,咱们想要执行什么样的动做,就在这里面实现。结合上面的例子解释:
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
//这里能够得到传给performLogin的参数
Object[] arguments = invocation.getArguments();
//callback是第三个参数
NetworkCallback callback = (NetworkCallback) arguments[2];
callback.onFailure(500, "Server error");
return 500;
}
}).when(mockUserManager).performLogin(anyString(), anyString(), any(NetworkCallback.class));
这里,当调用 mockUserManager 的 performLogin 方法时,会执行answer里面的代码,咱们上面的例子是直接调用传入的 callback 的 onFailure 方法,同时传给 onFailure 方法500和"Server error"。
固然,使用 Mockito.doAnswer() 须要建立一个Answer对象,这有点麻烦,代码看起来也繁琐,若是想简单的指定目标方法“什么都不作”,那么可使用 Mockito.doNothing() 。若是想指定目标方法“抛出一个异常”,那么可使用 Mockito.doThrow(desiredException) 。若是你想让目标方法调用真实的逻辑,可使用 Mockito.doCallRealMethod() 。(什么??? 默认不是会这样吗??? No! )
最后介绍一个Spy的东西。前面咱们讲了mock对象的两大功能,对于第二大功能: 指定方法的特定行为,不知道你会不会好奇,若是我不指定的话,它会怎么样呢?那么如今补充一下,若是不指定的话,一个mock对象的全部非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不作。然而不少时候,你但愿达到这样的效果:除非指定,否者调用这个对象的默认实现,同时又能拥有验证方法调用的功能。这正好是spy对象所能实现的效果。建立一个spy对象,以及spy对象的用法介绍以下:
//假设目标类的实现是这样的
public class PasswordValidator {
public boolean verifyPassword(String password) {
return "xiaochuang_is_handsome".equals(password);
}
}
@Test
public void testSpy() {
//跟建立mock相似,只不过调用的是spy方法,而不是mock方法。spy的用法
PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);
//在默认状况下,spy对象会调用这个类的真实逻辑,并返回相应的返回值,这能够对照上面的真实逻辑
spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
//spy对象的方法也能够指定特定的行为
Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
//一样的,能够验证spy对象的方法调用状况
spyValidator.verifyPassword("xiaochuang_is_handsome");
Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
}
总之,spy与mock的惟一区别就是默认行为不同:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不作,或直接返回默认值。
这篇文章介绍了mock的概念以及Mockito的使用,可能Mockito的不少的一些其余方法没有介绍,但这只是阅读文档的问题而已,更重要的是理解mock的概念。
若是你想了解Mockito更详细的用法能够参考 这篇文章 ,写的是至关的好。
下一篇文章咱们将介绍依赖注入的概念,以及(或许)使用dagger2来更方便的作依赖注入,以及在单元测试里面的应用,这里依而后不少的误区,须要你们注意的,想知道具体是什么吗?那就Stay tuned!
文中代码在: Github这个项目
有任何意见或建议,或者发现文中任何问题,欢迎留言!
来源:小创