Mockito

midoll 315 2023-02-25

Mockito(一) – 入门篇

Mockito是一种mock工具/框架。我理解EasyMock有点过时了,Mockito是现在比较流行的。

一,Mockito是干什么的?

例如现在我们写了一个Java类,该类扫描一个目录并将找到的文件都上传到FTP server。该类对于不同的FTP响应(找不到FTP server 或 上传成功,或上传失败),有一些后续操作。

在写这个类的UT时,我们就必须虚构出来一个FTP对象。这样在UT中,这个虚构的对象能够代替真正的FTP,对被测试类的调用做出一定的响应,从而知道被测试类是否正确的调用了FTP并做出一些正确的期望的响应。例如,我们希望调用一个虚构的FTP对象,强制在调用该对象时返回“找不到指定的FTP Server”,并测试被测试类是否记录了错误日志。

而Mockito就提供了一系列的方法,便于大家在UT中构建虚拟的对象(例如上例中的虚拟的FTP Server),并使用。

二,我们使用Mockito做测试时,通常有如下三步。

  • 如前面所说,我们需要创建一个mock对象来代替真的对象。因此,模拟对象是第一步。在Mockito中,模拟对象使用mock()方法。

  • 强制mock对象被调用时的返回值是第二步,比如指定让虚构的FTP对象第一次被调用时返回"找不到FTP server"。这一步一般称为stubbing。一般是when(mockedList.get(0)).thenReturn(“first”)的样子。

  • 验证被测试类是否正确工作是第三步,使用verify()。例如,验证当虚构的FTP对象返回"找不到FTP server"时,测试被测试类是否再次尝试连接FTP并记录日志。

经过这三步测试过程就完成啦!下面我们对第一步和第二步做说明。第三步我们在后续文章中再做讲解。

a, 模拟对象:

// 模拟LinkedList 的一个对象  
LinkedList mockedList = mock(LinkedList.class);   
  
// 此时调用get方法,会返回null,因为还没有对方法调用的返回值做模拟   
System.out.println(mockedList.get(999)); 

b, 模拟方法调用的返回值:

// 模拟获取第一个元素时,返回字符串first。  给特定的方法调用返回固定值在官方说法中称为stub。
when(mockedList.get(0)).thenReturn("first");   
  
// 此时打印输出first   
System.out.println(mockedList.get(0)); 

c, 模拟方法调用抛出异常:

// 模拟获取第二个元素时,抛出RuntimeException  
when(mockedList.get(1)).thenThrow(new RuntimeException());   
  
// 此时将会抛出RuntimeException  
System.out.println(mockedList.get(1));  
 没有返回值类型的方法也可以模拟异常抛出:
 
doThrow(new RuntimeException()).when(mockedList).clear();

d, 模拟调用方法时的参数匹配:

// anyInt()匹配任何int参数,这意味着参数为任意值,其返回值均是element  
when(mockedList.get(anyInt())).thenReturn("element");   
  
// 此时打印是element   
System.out.println(mockedList.get(999)); 

e, 模拟方法调用次数:

// 调用add一次   
mockedList.add("once");   
  
// 下面两个写法验证效果一样,均验证add方法是否被调用了一次  
verify(mockedList).add("once");   
verify(mockedList, times(1)).add("once");  

另外还可以通过atLeast(int i)和atMost(int i)来替代time(int i)来验证被调用的次数最小值和最大值。

01 Mockito相关

01::01 前期准备

import org.mockito.InjectMocks;
import org.mockito.Mock;
@InjectMocks
ServiceTest serviceTest;
@Mock
ServiceTestMapper mapper;

@Mock:模拟出一个Mock对象,对象是空的,需要指明对象调用什么方法,传入什么参数时,返回什么值
@InjectMocks:依赖@Mock对象的类,也即是被测试的类。@Mock出的对象会被注入到@InjectMocks对象中

01::02 单元测试注解

@RunWith(PowerMockRunner.class)
@PrepareForTest({})
@PowerMockIgnore({"javax.net.ssl.*","javax.management.*"})

01::03 为UT提供框架使用的自动验证

import org.mockito.MockitoAnnotations;
MockitoAnnotations.initMocks(this);

01::04 给测试类中的变量赋值

import org.springframework.test.util.ReflectionTestUtils;
ReflectionTestUtils.setField(serviceTest,"变量名","要赋给该变量的值");

01::05 给测试类中的方法设置返回值

import org.powermock.api.mockito.PowerMockito;
import static org.mockito.ArgumentMatchers.any;
PowerMockito.when(mapper.test(any(String.class))).thenReturn(返回内容)

01::06 给测试类中的方法抛出异常

import org.powermock.api.mockito.PowerMockito;
import static org.mockito.ArgumentMatchers.any;
PowerMockito.when(mapper.test(any(String.class))).thenThrow(new 异常);

01::07 给测试类中的方法设置不返回对象

import org.powermock.api.mockito.PowerMockito;
import static org.mockito.ArgumentMatchers.any;
PowerMockito.doNothing().when(serviceTest).mapper.test(any(String.class));

01::08 Mock方法内部new出来的对象

例子:
File file = new File(path);
file.exists();
单元测试:
File file1 = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withArguments(file).thenReturn(file1);
PowerMockito.when(file1.exists()).thenReturn(true);

01::09 给测试类中的静态类调静态方法提供返回值

测试类头部:
@PrepareForTest({静态类.class})
前期准备:
@Before
public void init(){
PowerMockito.mockStatic(静态类.class);
}
调用:
PowerMockito.when(静态类.静态方法()).thenReturn(返回内容);

02 辅助Mockito

02 ::01 junit常用注解

junit常用注解

@Test(timeout = 10)
测试,该注解必须加到方法上
timeout超时时间,单位是毫秒
终止死循环,当达到设定的值,结束循环
@Ignore
忽略不想被测试的方法,该注解必须加到方法上,也可以加到类上(慎用)
@RunWith(SpringJUnit4ClassRunner.class)
把junit和spring整合到一块,该注解加到类上
@ContextConfiguration(locations = {“classpath:conf/applicationContext.xml”})
用于加载spring配置文件的注解,添加到类上
locations代表spring配置文件路径的数组,数组的类型为Stirng
classpath:这个东西代表从源包下开始寻找
@Resource(name = “blogService”)
注入属性的注解,就相当于set、get方法,name指明bean的id值
@Before
在所有方法之前执行,一般加到方法上
@After
在所有方法之后执行,一般加到方法上
@Transactional
@TransactionConfiguration(transactionManager = “transactionManager”, defaultRollback = true)
上边这俩注解要一块用,用于事物控制,加到类上
transactionManager代表配置文件中事务管理器bean的id值
defaultRollback代表事物回滚,默认值为true,是回滚的

02:02 assert常用方法

Assert.assertEquals(“message”,A,B):
判断对象A和B是否相等,这个判断比较时调用了equals()方法。
Assert.assertSame(“message”,A,B):
判断对象A和B是否相同,使用的是==操作符。
Assert.assertTure(“message”,A):
判断A条件是否为真。
Assert.assertFalse(“message”,A):
判断A条件是否不为真。
Assert.assertNotNull(“message”,A):
判断A对象是否不为null
Assert.assertArrayEquals(“message”,A,B):
判断A数组与B数组是否相等。
等等.............................................

实例介绍

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) {
             return (soapMessage instanceof SOAPMessage) || soapMessage==null;
      }
 }
 
//上面的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;

# 单元测试