Mockito是一种mock工具/框架。我理解EasyMock有点过时了,Mockito是现在比较流行的。
什么是mock?说的直白一点,大家都知道unit test应该是尽可能独立的。对一个class的unit test不应该再和其他class有任何交互。
现在有一个类,扫描一个目录并将找到的文件都上传到FTP server。该类对于不同的FTP响应(找不到FTP server 或 上传成功,或上传失败),有一些后续操作。
在写这个类的UT时,我们就必须虚构出来一个FTP对象。这样在UT中,这个虚构的对象能够代替真正的FTP,对被测试类的调用做出一定的响应。从而知道被测试类是否正确的调用了FTP并做出一些正确的期望的响应。从而达到测试的目的。
mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。
关于mock的概念和EasyMock,可以参考:
Mock object and EasyMock framework
http://blog.csdn.net/OnlyQi/archive/2011/04/26/6364885.aspx
一篇很好的入门文章:
http://blog.csdn.net/huoshuxiao/archive/2010/12/30/6107835.aspx
一些稍微复杂且实用一点的例子:
http://gojko.net/2009/10/23/mockito-in-six-easy-examples/
下面介绍mockito的基本使用方法。
如前面所说,我们需要创建一个mock对象来代替真的对象。
因此,
模拟对象是第一步。模拟对象使用mock();
指定mock对象被调用时的返回值是第二步,比如指定让mockFTP对象第一次被调用时返回"找不到FTP server"。这一步一般称为stubbing。一般是when(mockedList.get(0)).thenReturn("first")的样子。
验证被测试类是否正确工作是第三步,使用verify()。例如,验证当mockFTP对象返回"找不到FTP server"时,测试代码是否按要求重试。
然后测试就完成啦~~
模拟对象:
- // 模拟LinkedList 的一个对象
- LinkedList mockedList = mock(LinkedList.class);
- // 此时调用get方法,会返回null,因为还没有对方法调用的返回值做模拟
- System.out.println(mockedList.get(999));
模拟方法调用的返回值:
比如
- // 模拟获取第一个元素时,返回字符串first。 给特定的方法调用返回固定值在官方说法中称为stub。
- when(mockedList.get(0)).thenReturn("first");
- // 此时打印输出first
- System.out.println(mockedList.get(0));
模拟方法调用抛出异常:
- // 模拟获取第二个元素时,抛出RuntimeException
- when(mockedList.get(1)).thenThrow(new RuntimeException());
- // 此时将会抛出RuntimeException
- System.out.println(mockedList.get(1));
doThrow(new RuntimeException()).when(mockedList).clear();
模拟调用方法时的参数匹配:
- // anyInt()匹配任何int参数,这意味着参数为任意值,其返回值均是element
- when(mockedList.get(anyInt())).thenReturn("element");
- // 此时打印是element
- System.out.println(mockedList.get(999));
模拟方法调用次数:
- // 调用add一次
- mockedList.add("once");
- // 下面两个写法验证效果一样,均验证add方法是否被调用了一次
- verify(mockedList).add("once");
- verify(mockedList, times(1)).add("once");
学习了基本知识后,就可以实战了。Mockito的实际使用还是比较麻烦的。
因为在实际使用中,最常遇到的就是需要模拟第三方类库的行为。
比如现在有一个类FTPFileTransfer,实现了向FTP传输文件的功能。这个类中使用了apache的ftp类
org.apache.commons.net.ftp.FTPClient;
现在测试FTPFileTransfer 这个类中的isFTPConnected方法, 希望模拟无法连接ftp的情况,测试是否记录了错误log:
public class FTPFileTransfer{
//为了测试当ftp链接不上时,是否真的会记log,我们必须mock一个假的FTPClient对象,用该对象传递/覆盖掉真实的FTPClient对象ftp,并强制让这个假对象返回"无法连接",然后看是否会记log.
private FTPClient ftp;
private boolean isFTPConnected(){
if (!ftp.isConnected()) {
LOGGER.error("Disconnected from FTP.");
}
}
因此使用mock需要解决的问题是: 如何用mock的FTP对象覆盖掉真实代码中调用的FTPClient。因此需要将mock对象传递进去。
这里对源代码有一个限制:
源代码中必须使用set和get方法来设置/获得ftp对象,这样测试代码可以使用set来传递mock对象。
或者测试代码中写一个方法覆盖掉源代码中实例化ftp对象的方法,且测试代码中使用mock ftp对象。
下面的示例中都假定源代码中使用了get/set。
- 创建一个返回FTPFileTransfer instance的方法。
//使用mock()方法创建一个FTP的mock对象mockedFTP。
FTPClient mockedFTP = mock(FTPClient.class);
//Stub “无法连接”
when(mockedFTP.isConnected())).thenReturn("无法连接");
//写一个get方法返回FTPFileTransfer的实例用来测试,将mockedFTP作为参数传递进去。同时在这个方法内部用set方法将FTPFileTransfer类中的成员变量FTPClient ftp更改为mockFTP。
//这样我们就得到了一个FTPFileTransfer的实例,同时里面的ftp已经变成了我们希望的mockFTP。
private FTPFileTransfer getMockTaskFileTransfer(final FTPClientmockedFTP) {
FTPFileTransfer test = new FTPFileTransfer("127.0.0.1", 8888, "//usr", "username", "password");
test.setFTPClient(mockedFTP);
return test;
}
@Test
public void testTransfer() throws SocketException, IOException{
FTPFileTransfer test = getMockTaskFileTransfer();
//得到这个实例以后,就直接调用这个实例的isFTPConnected方法,然后去log文件里找有没有我们希望的log就行了。注意此时,mockedFTP一定会返回"无法连接",所以isFTPConnected一定会记log。
test.isFTPConnected();
}
测试完毕~~~
- 用subclass-and-override实现
从名字就可以看出,通过创建被测试类的子类,覆盖掉被测试类的getFTPClient()方法,将mock对象传递进去。
class MockFTPFileTransfer extends FTPFileTransfer{
public MockFTPFileTransfer(){
super("127.0.0.1", 8888, "//usr", "username", "password");
}
//源代码中必须使用get来获得ftp对象,否则mock不会生效
@Override
public FTPClient getFTPClient(){
FTPClient mockedFTP = mock(FTPClient.class);
when(mockedFTP.isConnected()).thenReturn(true);
return mockedFTP;
}
}
@Test
public void testTransfer() throws SocketException, IOException{
FTPFileTransfer test = new MockFTPFileTransfer();
test.isFTPConnected();
}
- 用partial mock实现
partial mock是1.8之后的新功能。通常情况下会使用mock出来的对象完全覆盖掉被模拟的对象,对于那些没有stub的方法,则会返回build-in 类型的默认值。
@Test
public void testTransfer() throws SocketException, IOException{
//模拟一个FTPClient对象,同时stub行为。
FTPClient mockedFTP = mock(FTPClient.class);
when(mockedFTP.isConnected()).thenReturn(true);
//spy可以模拟一个real object
//这里的可以认为是spy在real object上包了一层,除了getFTPClient()被覆盖掉以外,其他方法仍然是真实对象的。
//这就是partial mock的概念: 仅仅用mock对象覆盖源对象的一部分,而不是全部。
FTPFileTransfer spyFTP= spy(new FTPFileTransfer("127.0.0.1", "//usr", "username", "password"));
when(spyFTP.getFTPClient()).thenReturn(mockedFTP);
spyFTP.setFTPClient(mockedFTP);
spyFTP.isFTPConnected();
}
回到官网:http://mockito.org/,打开documentation可以看到原文。
强烈建议不熟悉Mockito的同学先看看我写的Mockito(一)入门篇和(二)实例篇之后再来看这篇文章。
因为只有看了前两篇文章才明白mockito的本质以及该如何使用它。
下面是按原文 翻译+注释 的对Mockito全部功能的介绍。
1, 使用mockito验证行为。
//首先要importMockito.
import static org.mockito.Mockito.*;
//mock creation
List mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//验证add方法是否在前面被调用了一次,且参数为“one”。clear方法同样。
verify(mockedList).add("one");
verify(mockedList).clear();
//下面的验证会失败。因为没有调用过add("two")。
verify(mockedList).add("two");
原文中的一句话很重要:Once created, mock will remember all interactions.所以mockito知道前面是否调用过某方法。
2, 使方法调用返回期望的值。也被称为stubbing
//You can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
//stubbing。当get(0)被调用时,返回"first". 方法get(1)被调用时,抛异常。
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
System.out.println(mockedList.get(0));
//following throws runtime exception
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
默认情况下,对于所有有返回值且没有stub过的方法,mockito会返回相应的默认值。
对于内置类型会返回默认值,如int会返回0,布尔值返回false。对于其他type会返回null。
这里一个重要概念就是: mock对象会覆盖整个被mock的对象,因此没有stub的方法只能返回默认值。
//重复stub两次,则以第二次为准。如下将返回"second":
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(0)).thenReturn("second");
//如果是下面这种形式,则表示第一次调用时返回“first”,第二次调用时返回“second”。可以写n多个。
when(mockedList.get(0)).thenReturn("first").thenReturn("second");
但是,如果实际调用的次数超过了stub过的次数,则会一直返回最后一次stub的值。
如上例,第三次调用get(0)时,则会返回"second".
3, 参数匹配
在上例中如果想实现get(任意整数)时,都返回“element”时,该怎么做呢?很简单。
//stubbing 使用了内置的anyint() matcher.
when(mockedList.get(anyInt())).thenReturn("element");
//因此除了anyint()之外,还有其他很多matcher。这里请参考原文。
//使用了matcher一样可以验证被调用的次数。
verify(mockedList).get(anyInt());
这里有一个限制就是,如果在调用方法时需要传入多个参数,其中一个参数使用了argument matcher,那么所有的参数必须都是matcher。
不可以matcher和实际的参数混着用。
这里也可以使用custom argument matcher。因为很多时候输入参数不是build-in 类型,而是我们自己写的一些类,或特殊对象。
这时要使用argument matcher,就必须订制特殊的matcher了。
下例是一个特殊的matcher的实例,这个matcher可以匹配任何file对象。
public class SayHiTest {
@Test
public void testSayHi() throws Exception {
File mock = mock(File.class); //首先mock File类。
//注意new IsAnyFiles()并不是一个matcher,需要调用argThat(new IsAnyFiles()))才返回一个matcher。
//下句中stub:当调用renameTo方法时,返回false。该方法参数可以是任意file对象。
when(mock.renameTo(argThat(new IsAnyFiles()))).thenReturn(false);
mock.renameTo(new File("test"));
//下句verify renameTo方法被调用了一次,同时输入参数是任意file。
verify(mock).renameTo(argThat(new IsAnyFiles()));
}
}
class IsAnyFiles extends ArgumentMatcher<File> {
public boolean matches(Object file) {
return file.getClass() == File.class;
}
}
另外一个参数匹配的例子:
class IsSOAPMessage extends ArgumentMatcher<SOAPMessage> {
public boolean matches(Object soapMessage) {
}
}
//上面的macther不仅可以匹配任意的SOAPMessage对象,如果输入参数为空也可以匹配上。这里说一下我犯过的一个错误。
我在做参数匹配的时候,没有考虑到输入参数为空的情况,导致matcher匹配不上,进而stub的行为无法生效。
其实在发现mock对象没有想自己想象的方式工作时,最好的方法就是debug进去,首先要先确定mock对象是不是真的传递进去了。然后再一步步的debug。
通常遇到的两种情况就是1,mock对象没有传递进去。2,参数没有匹配上。
4, 验证方法被调用了特定次数/至少x次/最多x次/从未被调用
//是否add("twice")被调用了两次。
verify(mockedList, times(2)).add("twice");
//验证add("twice")被调用了至少一次。以及其他。
verify(mockedList, atLeastOnce()).add("twice");
verify(mockedList, atLeast(2)).add("twice");
verify(mockedList, atMost(5)).add("twice");
verify(mockedList, never()).add("twice");
5, 调用方法时抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();
后面还会再介绍几个类似的方法,例如doReturn()。
6, 验证顺序
//下面的代码验证firstMock先被调用,secondMock后被调用。
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
7, 验证mock之间没有相互作用6,7都不是很明白实际意义是什么。
8, 找到冗余的调用
用never()就可以实现,不多说
9, 使用@mock 定义mock。
之前都是使用mock()来模拟一个对象。用@mock是一个shorthand。
public class ArticleManagerTest {
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;