Mockito使用详解

一、介绍

Mockito 是一个针对 Java 的单元测试模拟框架,可以简化单元测试过程中测试上下文对象。

它可以做如下事情:

  • 模拟方法的返回值、模拟抛出异常
  • 验证方法被调用次数、验证方法参数类型
  • 捕获方法参数值
  • 为真实对象创建一个监控(spy)对象

注意:

  • 不能 Mock 静态方法
  • 不能 Mock private 方法
  • 不能 Mock final class

概念介绍

1)Mockito:简单轻量级的做mocking测试的框架;
2)mock对象:在调试期间用来作为真实对象的替代品;
3)mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试;
4)stub:存根,就是为mock对象的方法指定返回值(可抛出异常);
5)verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等);

二、使用介绍

spring-boot-starter-test已经集成了mockito,所以SpringBoot项目无法另外引入依赖。

如果非SpringBoot项目可以类似如下引入:

<dependency>
  <groupId>org.junit.jupitergroupId>
  <artifactId>junit-jupiterartifactId>
  <version>5.8.2version>
  <scope>compilescope>
dependency>
<dependency>
  <groupId>org.mockitogroupId>
  <artifactId>mockito-coreartifactId>
  <version>4.5.1version>
  <scope>compilescope>
dependency>
<dependency>
  <groupId>org.mockitogroupId>
  <artifactId>mockito-junit-jupiterartifactId>
  <version>4.5.1version>
  <scope>compilescope>
dependency>

常用方法

方法名 描述
Mockito.mock(classToMock) 模拟对象
Mockito.mock(classToMock, defaultAnswer) 使用默认Answer模拟对象
Mockito.verify(mock) 验证行为是否发生
Mockito.when(methodCall).thenReturn(value) 设置方法预期返回值
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) //等价于Mockito.when(methodCall).thenReturn(value1, value2) 触发时第一次返回value1,第n次都返回value2
Mockito.when(methodCall).thenAnswer(answer)) 预期回调接口生成期望值
Mockito.doThrow(toBeThrown).when(mock).[method] 模拟抛出异常。
Mockito.doReturn(toBeReturned).when(mock).[method] 设置方法预期返回值(直接执行不判断)
Mockito.doAnswer(answer).when(methodCall).[method] 预期回调接口生成期望值(直接执行不判断)
Mockito.doNothing().when(mock).[method] 不做任何返回
Mockito.doCallRealMethod().when(mock).[method] //等价于Mockito.when(mock.[method] ).thenCallRealMethod(); 调用真实的方法
Mockito.spy(Object) 用spy监控真实对象,设置真实对象行为
Mockito.inOrder(Object… mocks) 创建顺序验证器
Mockito.reset(mock) 重置mock

1、简单使用

一旦mock对象被创建了,mock对象会记住所有的交互,然后你就可以选择性的验证你感兴趣的交互,验证不通过则抛出异常。

mock函数默认返回的是null、一个空的集合或者一个被对象类型包装的内置类型(例如0、false对应的对象类型为Integer、Boolean)。

一旦测试桩函数被调用,该函数将会一直返回固定的值。

@Test
@DisplayName("mock简单使用")
public void test1() {
    // 创建一个Mock对象
    List mockedList = Mockito.mock(List.class);

    //存根方法,调用get(0)时,返回first
    Mockito.when(mockedList.get(0)).thenReturn("first");
    // 以下代码实现相同的效果
    // Mockito.doReturn("first").when(mockedList).get(0);
    //返回first
    System.out.println(mockedList.get(0));
    //没有存根,则会返回null
    System.out.println(mockedList.get(999));
    // 验证方法被调用次数,指定参数类型匹配器
    Mockito.verify(mockedList, times(2)).get(anyInt());
    //存根方法,调用get(1)时,直接抛出异常
    Mockito.when(mockedList.get(1)).thenThrow(new RuntimeException());
    // 以下代码实现相同的效果
    // Mockito.doThrow(new RuntimeException()).when(mockedList).get(1);
    //抛出异常
    System.out.println(mockedList.get(1));
}

2、参数匹配器

@Test
public void argMatch(){
    // 创建一个Mock对象
    List<String> mockedList = Mockito.mock(List.class);
    //使用内置的 anyInt() 参数匹配器
    when(mockedList.get(anyInt())).thenReturn("element");

    //使用自定义匹配器 (假设 isValid() 返回你自己的匹配器实现):
    when(mockedList.contains(argThat(isValid()))).thenReturn(true);

    //打印 "element"
    System.out.println(mockedList.get(999));

    //验证方法调用时,使用参数匹配器
    verify(mockedList).get(anyInt());
    mockedList.add("12345");
    //使用 Java 8 Lambdas 实现参数匹配器
    verify(mockedList).add(argThat(someString -> someString.length() > 5));
}

如果你使用了参数匹配器,方法中的所有参数都必须是匹配器。

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//上面是正确的 - eq() 也是一个参数匹配器
 
verify(mock).someMethod(anyInt(), anyString(), "third argument");
//上面代码是错误的 - 因为第三个参数不是参数匹配器

参数匹配器列表

org.mockito.ArgumentMatchers中定义了所有的内置匹配器

函数名 说明
any() 任意类型
any(Class type) 任意指定的Class类型,除了null
isA(Class type) 指定类型的实现对象
anyInt() 任何int或者non-null Integer
anyXxx() 其他类似还有(Boolean、Byte、Char、Int、Long、Float、Double、Short、String、List、Set、Map、Collection、Iterable)同样必须非空
eq(value) 等于给定的值
same(value) 和给定的值是同一个对象
isNull() null值
notNull() 非null
nullable(Class clazz) null 或者给定的类型
contains(String substring) 包含指定的字符串
matches(String regex) 匹配正则表达式
endsWith(String suffix) 以xx结尾
startsWith(String prefix) 以xx开头
argThat(ArgumentMatcher matcher) 自定义匹配器

3、验证精确调用次数

verify()默认验证方法被调用1次,可以传入times()方法,匹配精确的次数,或者其他类似方法

  • times(n),匹配n次
  • never(),没被调用,等于times(0)
  • atMostOnce(),最多1次
  • atLeastOnce(),最少1次
  • atLeast(n),最少n次
  • atMost(n),最多n次
//using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

4、验证执行顺序

可以通过Mockito.inOrder(Object... mocks)创建顺序验证器

// A. 单个mock对象调用顺序验证
List singleMock = mock(List.class);

//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

//创建顺序验证器,使用单个mock
InOrder inOrder = inOrder(singleMock);

//以下代码验证先调用 "was added first", 然后调用 "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");

// B. 组合 mocks 调用顺序验证
List firstMock = mock(List.class);
List secondMock = mock(List.class);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

//创建顺序验证器,使用多个mock
InOrder inOrder = inOrder(firstMock, secondMock);

//以下代码验证 firstMock 在 secondMock 之前调用
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

5、监控真实对象

可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。

List list = new LinkedList();
List spy = spy(list);

//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

//using the spy calls *real* methods
spy.add("one");
spy.add("two");

//prints "one" - the first element of a list
System.out.println(spy.get(0));

//size() method was stubbed - 100 is printed
System.out.println(spy.size());

//optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");

重要提示!
有时,spy对象不能使用when(Object)来stub方法,请考虑使用doReturn|Answer|Throw()系列方法来stub方法,看下面例子:

List list = new LinkedList();
List spy = spy(list);

//注意: 真实对象方法会被调用, 所以spy.get(0) 会抛出异常 IndexOutOfBoundsException (list 目前还是空的)
when(spy.get(0)).thenReturn("foo");

//可以使用 doReturn() 来存根
doReturn("foo").when(spy).get(0);

6、自定义验证失败信息

@Test
public void test() {
    final ArrayList arrayList = mock(ArrayList.class);
    arrayList.add("one");
    arrayList.add("two");

    verify(arrayList, description("size()没有调用")).size();
    // org.mockito.exceptions.base.MockitoAssertionError: size()没有调用

    verify(arrayList, timeout(200).times(3).description("验证失败")).add(anyString());
    //org.mockito.exceptions.base.MockitoAssertionError: 验证失败
}

7、参数捕捉器

ArgumentCaptor argument = ArgumentCaptor.forClass(Class clazz) 创建指定类型的参数捕获器

argument.capture() 捕获方法参数

argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值

argument.getAllValues() 方法进行多次调用后,返回多个参数值

@Test
@DisplayName("参数捕捉器")
public void argumentCaptor() {
    List mock = mock(List.class);
    List mock1 = mock(List.class);
    mock.add("John");
    mock1.add("Brian");
    mock1.add("Jim");
    // 获取方法参数
    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
    verify(mock).add(argument.capture());
    System.out.println(argument.getValue());    //John

    // 多次调用获取最后一次
    ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);
    verify(mock1, times(2)).add(argument1.capture());
    System.out.println(argument1.getValue());    //Jim

    // 获取所有调用参数
    System.out.println(argument1.getAllValues());    //[Brian, Jim]
}

8、doAnswer回调函数

@Test
@DisplayName("设置方法回调函数")
public void mockListAnswer() {
    List mockedList = mock(List.class);
    //  Mockito.when(mockedList.get(Mockito.anyInt())).thenAnswer(invocationOnMock -> {
    //   System.out.println("哈哈哈,被我逮到了吧");
    //   Object[] arguments = invocationOnMock.getArguments();
    //   System.out.println("参数为:" + Arrays.toString(arguments));
    //   Method method = invocationOnMock.getMethod();
    //   System.out.println("方法名为:" + method.getName());
    //   return "结果由我决定";
    //  });

    Mockito.doAnswer(invocationOnMock -> {
        System.out.println("哈哈哈,被我逮到了吧");
        Object[] arguments = invocationOnMock.getArguments();
        System.out.println("参数为:" + Arrays.toString(arguments));
        Method method = invocationOnMock.getMethod();
        System.out.println("方法名为:" + method.getName());
        return "结果由我决定";
    }).when(mockedList).get(anyInt());

    System.out.println(mockedList.get(0));
}

9、设置mock默认行为

// 创建mock对象、使用默认返回
final ArrayList mockList = mock(ArrayList.class);
System.out.println(mockList.get(0));    //null

// 这个实现首先尝试全局配置,如果没有全局配置就会使用默认的回答,它返回0,空集合,null,等等。
// 参考返回配置:ReturnsEmptyValues
mock(ArrayList.class, Answers.RETURNS_DEFAULTS);

// ReturnsSmartNulls首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回SmartNull。
// 如果最终返回对象,那么会简单返回null。一般用在处理遗留代码。
// 参考返回配置:ReturnsMoreEmptyValues
mock(ArrayList.class, Answers.RETURNS_SMART_NULLS);

// 未stub的方法,会调用真实方法。
//    注1:存根部分模拟使用时(mock.getSomething ()) .thenReturn (fakeValue)语法将调用的方法。对于部分模拟推荐使用doReturn语法。
//    注2:如果模拟是序列化反序列化,那么这个Answer将无法理解泛型的元数据。
mock(ArrayList.class, Answers.CALLS_REAL_METHODS);

// 深度stub,用于嵌套对象的mock。参考:https://www.cnblogs.com/Ming8006/p/6297333.html
mock(ArrayList.class, Answers.RETURNS_DEEP_STUBS);

// ReturnsMocks首先尝试返回普通值(0,空集合,空字符串,等等)然后它试图返回mock。
// 如果返回类型不能mocked(例如是final)然后返回null。
mock(ArrayList.class, Answers.RETURNS_MOCKS);

//  mock对象的方法调用后,可以返回自己(类似builder模式)
mock(ArrayList.class, Answers.RETURNS_SELF);

// 自定义返回
final Answer<String> answer = new Answer<String>() {
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        return "test_answer";
    }
};
final ArrayList mockList1 = mock(ArrayList.class, answer);
System.out.println(mockList1.get(0));   //test_answer

10、配合注解使用@Mock、@Spy

注解必须结合扩展@ExtendWith(MockitoExtension.class)一起使用,否则无法自动创建对象

  • @Mock 创建一个Mock对象
  • @Spy 创建一个Spy对象(不能是接口或抽象类)
  • @InjectMocks 被测试类标注为@InjectMocks时(不能是接口或抽象类),会自动实例化,并且把@Mock或者@Spy标注过的依赖注入进去
@ExtendWith(MockitoExtension.class)
@DisplayName("mock单元测试")
public class Mockito2Test {

    // 创建一个Mock对象
    @Mock
    private List mockedList;
    // 创建一个Spy对象,不能是接口或抽象类
    @Spy
    private ArrayList spyList;
    
    // RegisterServiceImpl 依赖 UserDao
    @InjectMocks
    private RegisterServiceImpl registerService;
    // 创建一个Mock对象
    @Mock
    private UserDao userDao;
    
    @Test
    @DisplayName("mock简单使用")
    public void test1() {
        //调用get(0)时,返回first
        Mockito.when(mockedList.get(0)).thenReturn("first");
        //返回first
        System.out.println(mockedList.get(0));
        //没有存根,则会返回null
        System.out.println(mockedList.get(999));
        // 验证方法被调用次数,指定参数类型匹配器
        Mockito.verify(mockedList, times(2)).get(anyInt());
    }

    @Test
    @DisplayName("spy函数")
    public void spyTest() {
        spyList.add("01");
        spyList.add("02");
        doReturn("first").when(spyList).get(anyInt());
        //返回first
        System.out.println(spyList.get(0));
        reset(spyList);
        System.out.println(spyList.get(0));
        //验证是否调用过get函数。这里的anyInt()就是一个参数匹配器。
        verify(spyList).get(anyInt());
    }
    
    @Test
    public void register() {
        UserEntity entity = new UserEntity();
        entity.setName("test");
        doReturn(entity).when(userDao).save(any(UserEntity.class));
        // 调用注册方法,实际调用的userDao.save()
        UserEntity register = registerService.register("123");
        // 返回entity
        System.out.println("register = " + register);
        // 存根save方法,预期返回上面的对象entity
        // 存根findByName方法,预期返回上面的对象entity
        doReturn(entity).when(userDao).findByName("123");
        // 调用getUser方法,返回上面的entity对象,调用userDao.findByName()
        UserEntity user = registerService.getUser("123");
        // 返回entity
        System.out.println("user = " + user);
        // 判断userDao的save方法是否被调用了一次
        verify(userDao).save(isA(UserEntity.class));
        // 判断userDao的findByName方法是否被调用了一次
        verify(userDao).findByName(anyString());
    }
}

三、集合SpringBoot使用

SpringBoot集成了Mockito,可以实现对Spring容器中的Bean对象进行mock。

直接引入以下依赖即可:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>

SpringBoot增加了注解@MockBean@SpyBean,结合@SpringBootTest使用可以自动将Spring容器中的Bean对象的依赖Bean替换为mock或spy对象

怎么理解这句话呢,以下代码示例:

1)RegisterServiceImpl依赖UserDao

public interface IRegisterService {
    UserEntity register(String name);
    UserEntity getUser(String name);
}

@Service
public class RegisterServiceImpl implements IRegisterService {

    @Resource
    private UserDao userDao;

    @Override
    @Transactional
    public UserEntity register(String name) {
        UserEntity entity = new UserEntity();
        entity.setName(name);
        return userDao.save(entity);
    }

    @Override
    public UserEntity getUser(String name) {
        return userDao.findByName(name);
    }
}

2)编写单元测试RegisterServiceTest

@SpringBootTest(classes = MockitoApp.class)
@DisplayName("用户注册单元测试")
public class RegisterServiceTest {
    @Autowired
    private IRegisterService registerService;
    @MockBean
    private UserDao userDao;

    @Test
    public void register() {
        UserEntity entity = new UserEntity();
        entity.setName("test");
        doReturn(entity).when(userDao).save(any(UserEntity.class));
        // 调用注册方法,实际调用的userDao.save()
        UserEntity register = registerService.register("123");
        // 返回entity
        System.out.println("register = " + register);
        // 存根save方法,预期返回上面的对象entity
        // 存根findByName方法,预期返回上面的对象entity
        doReturn(entity).when(userDao).findByName("123");
        // 调用getUser方法,返回上面的entity对象,调用userDao.findByName()
        UserEntity user = registerService.getUser("123");
        // 返回entity
        System.out.println("user = " + user);
        // 判断userDao的save方法是否被调用了一次
        verify(userDao).save(isA(UserEntity.class));
        // 判断userDao的findByName方法是否被调用了一次
        verify(userDao).findByName(anyString());
    }
}

参考

  • https://site.mockito.org/
  • https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
  • https://juejin.cn/post/7202666869965520952

你可能感兴趣的:(测试,junit,java,单元测试)