参考文档:

Mockito教程
【java】单元测试Mockito中的Mock和Spy
@InjectMocks
Mockito Verify常见用法

1. Mockito 介绍

1.1 Mockito 是什么

Mockito 是 mocking 框架,它让你用简洁的 API 做测试。

1.2 为什么需要 Mock

测试驱动的开发( TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。如下图所示:

484791-20170120134044703-96948251

为了测试类 A,我们需要 Mock B 类和 C 类(用虚拟对象来代替)如下图所示:

484791-20170120134311703-125872357

Mock 是类的实例,是一个虚拟对象,并不是实际对象,不太好理解,看下面例子。

下面例子基于引用

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

2. 实例

2.1 验证行为

@Test
public void verify_behaviour(){
	//模拟创建一个List对象	
	List mock = mock(List.class);
	//使用mock的对象
	mock.add(1);
	mock.clear();
	// 验证add(1)和clear()行为是否发生
	verify(mock).add(1);
	verify(mock).clear();
}

2.2 模拟我们所期望的值

@Test
public void when_thenReturn(){
    // mock一个Iterator类
    Iterator iterator = mock(Iterator.class);
    // 预设当iterator调用next()时第一次返回hello,第n次都返回world
    when(iterator.next()).thenReturn("hello").thenReturn("world");
    // 使用mock的对象
    String result = iterator.next() + " " + iterator.next() + " " + iterator.next();
    // 验证结果
    assertEquals("hello world world",result);
}
@Test
public void with_arguments(){
    Comparable comparable = mock(Comparable.class);
    //预设根据不同的参数返回不同的结果
    when(comparable.compareTo("Test")).thenReturn(1);
    when(comparable.compareTo("Omg")).thenReturn(2);
    assertEquals(1, comparable.compareTo("Test"));
    assertEquals(2, comparable.compareTo("Omg"));
    //对于没有预设的情况会返回默认值
    assertEquals(0, comparable.compareTo("Not stub"));
}
@Test
public void with_unspecified_arguments(){
    List list = mock(List.class);
    //匹配任意参数
    when(list.get(anyInt())).thenReturn(1);
    when(list.contains(argThat(new IsValid()))).thenReturn(true);
    assertEquals(1, list.get(1));
    assertEquals(1, list.get(999));
    assertTrue(list.contains(1));
    assertTrue(!list.contains(3));
}

2.3 @Mock

在上面的测试中我们在每个测试方法里都mock了一个List对象,为了避免重复的mock,是测试类更具有可读性,我们可以使用下面的注解方式来快速模拟对象:

@Mock
private List mockList;

运行这个测试类你会发现报错了,mock的对象为NULL,为此我们必须在基类中添加初始化mock的代码

public class MockitoExample2 {
    @Mock
    private List mockList;

    public MockitoExample2(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shorthand(){
        mockList.add(1);
        verify(mockList).add(1);
    }
}

或者使用 MockitoExtension

@ExtendWith(MockitoExtension.class)
public class MockitoExample2 {
    @Mock
    private List mockList;

    public MockitoExample2(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shorthand(){
        mockList.add(1);
        verify(mockList).add(1);
    }
}

2.4 @Spy

  • Mock不是真实的对象,它只是用类型的class创建了一个虚拟对象,并可以设置对象行为
  • Spy是一个真实的对象,但它可以设置对象行为
  • 设置 spy 逻辑时不能再使用 when(某对象.某方法).thenReturn(某对象) 的语法,而是需要使用 doReturn(某对象).when(某对象).某方法 或者 doNothing(某对象).when(某对象).某方法
public class Main {
 
    public void fun(String s) {
        System.out.println(s + " : fun");
        fun1(s);
        fun2(s);
    }
 
    public void fun1(String s) {
        System.out.println(s + " : fun1");
    }
 
    private void fun2(String s) {
        System.out.println(s + " : fun2");
    }
 
    public int getVal(){
        return 5;
    }
}

mock 使用实例

  1. 使用 doCallRealMethod().when() 调用函数真正部分。
  2. 使用 when().thenReturn 自定义函数返回值。
@ExtendWith(MockitoExtension.class)
 public class MainTest {
 
    @Mock
    Main mockMain;
 
    @Test
    public void testFun() {
        // 执行mock,而不是真正部分,所以没有打印任何信息
        mockMain.fun("mock test One");
 
        // doCallRealMethod声明后,执行真正部分
        // 但是Mock只能对public(fun1)和protected函数进行mock
        // 对private函数(fun2)仍执行真正部分
        // 所以输出fun和fun2
        doCallRealMethod().when(mockMain).fun(anyString());
        mockMain.fun("mock test Two");
 
        // 执行mock,输出int的默认值0,而不是5
        System.out.println("val: " + mockMain.getVal());
        // when声明后,既不走真正部分,也不走mock,直接返回thenReturn()中定义的值
        // 注意:该值的类型需要和when中函数返回值类型一致
        when(mockMain.getVal()).thenReturn(10);
        System.out.println("val: " + mockMain.getVal());
    }
 
}

20180729194933914

Spy 使用实例

使用 when().thenReturn 自定义函数返回值。

@ExtendWith(MockitoExtension.class)
public class MainTest {
 
    @Spy
    Main spyMain;
 
    @Test
    public void testFun() {
        // 执行真正部分
        spyMain.fun("mock test One");
 
        // 执行真正部分
        System.out.println("val: " + spyMain.getVal());
        // 自定义返回值
        when(spyMain.getVal()).thenReturn(10);
        System.out.println("val: " + spyMain.getVal());
    }
}

20180729194948236

2.5 @InjectMocks

  • @InjectMocks:创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

  • spring 使用 @Autowird 等方式完成自动注入。在单元测试中,没有启动 spring 框架,@Autowird 无法自动注入,此时就需要通过 @InjectMocks完成依赖注入。

  • @InjectMocks 是无法注入其他 @InjectMocks 字段的,比如:

    @ExtendWith(MockitoExtension.class)
    public class MyControllerTest {
    
        @Mock
        private MyRepository myRepository;
    
        @InjectMocks
        private MyService myService;
    
        @InjectMocks
        private MyController myController;
    
        @Test
        public void doSomething() throws Exception {
            this.myController.doSomething();
        }
    }
    

2.6 @ExtendWith(SpringExtension.class)

当涉及Spring时

如果您想在测试中使用 Spring 测试框架功能(例如)@MockBean,则必须使用 @ExtendWith(SpringExtension.class)。它取代了不推荐使用的 JUnit4 @RunWith(SpringJUnit4ClassRunner.class)。

引入后,在 springboot 项目中,依旧无法使用 @Autowired, 无法加载 bean,只是配置了容器。

当不涉及Spring时

例如,如果您只想涉及Mockito而不必涉及Spring,那么当您只想使用 @Mock/ @InjectMocks 批注时,您就想使用 @ExtendWith(MockitoExtension.class),因为它不会加载到很多不需要的 Spring 东西中。它替换了不推荐使用的 JUnit4 @RunWith(MockitoJUnitRunner.class)。

2.7 @MockBean

我们可以使用 @mockBean 注解将 Mock 对象添加到 Spring 上下文中。

Mock 将替换 Spring 上下文中任何相同类型的现有 bean,如果没有定义相同类型的 bean,将添加一个新的 bean。

2.8 @SpyBean

注入真实对象,受 spring 管理,相当于自动替换对应类型 bean 的注入,比如 @Autowired 注入。

@SpyBean 解决了 SpringBoot 的单元测试中 @MockBean 不能 mock 库中自动装配的 Bean 的局限。使 SpringBoot 的单元测试更灵活也更简单。

@ExtendWith(SpringExtension.class)
public class MyTest {

    @MockBean
    private MyMapper mapper;

    @SpyBean
    private MyService myService;

    @Test
    void test() {
        System.out.println(myService.getUsername());
    }

}