[转] Mock以及Mockito的使用

http://www.open-open.com/lib/view/open1462177583813.html [From] http://www.open-open.com/lib/view/open1462177583813.htmlhtml

几点说明:java

  1. 代码中的 //<== 表示跟上面的相比,这是新增的,或者是修改的代码,不知道怎么样在代码块里面再强调几行代码T_T。。。android

  2. 不少时候,为了不中文歧义,我会用英文表述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就是建立一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:单元测试

  1. 验证这个对象的某些方法的调用状况,调用了多少次,参数是什么等等测试

  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动做

要使用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的功能和使用了。

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

最后介绍一个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这个项目

有任何意见或建议,或者发现文中任何问题,欢迎留言!

Android 单元测试: 首先,从是什么开始

Android单元测试(二):再来谈谈为何

Android单元测试(三):JUnit单元测试框架的使用

来源:小创

相关文章
相关标签/搜索