Mockito基础入门

Mockito 快速入门教程

  1. 概述

    Mockito 是一个流行的Java单元测试Mock框架,用于接口和数据模拟。

  2. 依赖安装

xml 复制代码
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
java 复制代码
 // 静态导入会使代码更简洁
 import static org.mockito.Mockito.*;

 // mock creation 创建mock对象
 List mockedList = mock(List.class);

 //using mock object 使用mock对象
 mockedList.add("one");
 mockedList.clear();

 //verification 验证
 verify(mockedList).add("one");
 verify(mockedList).clear();

一旦mock对象被创建了,mock对象会记住所有的交互。然后你就可能选择性的验证你感兴趣的交互。

如何做一些测试桩 (Stub)?

java 复制代码
 //You can mock concrete classes, not only interfaces
 // 你可以mock具体的类型,不仅只是接口
 LinkedList mockedList = mock(LinkedList.class);

 //stubbing
 // 测试桩
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(1)).thenThrow(new RuntimeException());

 //following prints "first"
 // 输出"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
 // 因为get(999) 没有打桩,因此输出null
 System.out.println(mockedList.get(999));

 //Although it is possible to verify a stubbed invocation, usually it's just redundant
 //If your code cares what get(0) returns then something else breaks (often before even verify() gets executed).
 //If your code doesn't care what get(0) returns then it should not be stubbed. Not convinced? See here.
 // 验证get(0)被调用的次数
 verify(mockedList).get(0);

默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;

测试桩函数可以被覆写 : 例如常见的测试桩函数可以用于初始化夹具,但是测试函数能够覆写它。请注意,覆写测试桩函数是一种可能存在潜在问题的做法;

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

上一次调用测试桩函数有时候极为重要-当你调用一个函数很多次时,最后一次调用可能是你所感兴趣的。

java 复制代码
import static org.mockito.Mockito.*;

// 创建mock对象 
// 你可以mock具体的类型,不仅只是接口
List mockedList = mock(List.class);
// 对于高版本Mockito 4.10.0+,可以写的更简洁
// List mockedList = mock();

// 下面添加测试桩(stubbing),指定mock的行为
// "当" 调用 mockedList.get(0) 返回 "first"
when(mockedList.get(0)).thenReturn("first");

// 下面代码将打印 "first"
System.out.println(mockedList.get(0));

// 下面将打印 "null",因为 get(999) 没有被打桩
System.out.println(mockedList.get(999));

上面示例,首先我们使用 Mockito 中的 mock 静态方法创建mock对象。或使用 @Mock 注解,通过 when()/given() 指定mock行为。例如上面当调用 mockedList.get(0) 将返回 "first",这一过程专业术语叫做"打桩"(stubbing)。

除了 mock()/@Mock,还可使用 spy()/@Spy,两者区别是 spy 是部分mock,如果不打桩执行的是真实的方法。

使用 @InjectMocks 注解实现依赖注入,自动将mock/spy对象注入到被测试对象中。

Mockito 中的 @Mock, @Spy, @Captor 及 @InjectMocks 注解

开始之前,我们需要先使 Mockito 注解生效,有几种方法:

方法一:在JUnit 上设置 MockitoJUnitRunner

java 复制代码
@ExtendWith(MockitoExtension.class)
public class MockitoAnnotationUnitTest {
    ...
}

方法二:手动编码,调用 MockitoAnnotations.openMocks() 方法

java 复制代码
@Before
public void init() {
    MockitoAnnotations.openMocks(this);
}

最后, 我们可以使用 MockitoJUnit.rule():

java 复制代码
public class MockitoAnnotationsInitWithMockitoJUnitRuleUnitTest {

    @Rule
    public MockitoRule initRule = MockitoJUnit.rule();

    ...
}

注意,这需要将rule 设置为 public

@Mock 注解

@Mock 是 Mockito 中用的最多的注解,我们用它来创建并注入mock对象,而不用手动调用 Mockito.mock 方法。

为了方便对比,下面这个例子中,我们先是手动mock一个ArrayList

java 复制代码
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
    List mockList = Mockito.mock(ArrayList.class);

    mockList.add("one");
    Mockito.verify(mockList).add("one");
    assertEquals(0, mockList.size());

    Mockito.when(mockList.size()).thenReturn(100);
    assertEquals(100, mockList.size());
}

然后我们通过 @Mock 注解的方式完成相同的工作:

java 复制代码
@Mock
List<String> mockedList;

@Test
public void whenUseMockAnnotation_thenMockIsInjected() {
    mockedList.add("one");
    Mockito.verify(mockedList).add("one");
    assertEquals(0, mockedList.size());

    Mockito.when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

@Spy 注解

spy与mock的区别是,mock代理了目标对象的全部方法,spy只是部分代理

下面我们学习如何使用 @Spy 注解spy一个现有的对象实例。

我们先不用注解的方式,演示如何创建一个 spy List。

java 复制代码
@Test
public void whenNotUseSpyAnnotation_thenCorrect() {
    List<String> spyList = Mockito.spy(new ArrayList<String>());

    spyList.add("one");
    spyList.add("two");

    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");

    assertEquals(2, spyList.size());

    Mockito.doReturn(100).when(spyList).size();
    assertEquals(100, spyList.size());
}

然后我们通过 @Spy 注解的方式完成相同的工作:

java 复制代码
@Spy
List<String> spiedList = new ArrayList<String>();

@Test
public void whenUseSpyAnnotation_thenSpyIsInjectedCorrectly() {
    spiedList.add("one");
    spiedList.add("two");

    Mockito.verify(spiedList).add("one");
    Mockito.verify(spiedList).add("two");

    assertEquals(2, spiedList.size());

    Mockito.doReturn(100).when(spiedList).size();
    assertEquals(100, spiedList.size());
}

本例中,我们:调用真实的 spiedList.add() 方法,向 spiedList 中新增元素使用Mockito.doReturn() 修饰后,spiedList.size() 会返回 100 而非 2

@Captor 注解

接下来让我们看看如何使用 @Captor 注解创建 ArgumentCaptor 实例。

在下面的示例中,我们先不使用 @Captor 注解,手动创建一个 ArgumentCaptor:

java 复制代码
@Test
public void whenUseCaptorAnnotation_thenTheSame() {
    List mockList = Mockito.mock(List.class);
    ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);

    mockList.add("one");
    Mockito.verify(mockList).add(arg.capture());

    assertEquals("one", arg.getValue());
}

现在,让我们使用 @Captor 注解来创建 ArgumentCaptor:

java 复制代码
@Mock
List mockedList;

@Captor 
ArgumentCaptor argCaptor;

@Test
public void whenUseCaptorAnnotation_thenTheSam() {
    mockedList.add("one");
    Mockito.verify(mockedList).add(argCaptor.capture());

    assertEquals("one", argCaptor.getValue());
}

@InjectMocks 注解

现在我们来讨论如何使用 @InjectMocks 注解将mock字段自动注入到被测试对象中。

在下面的示例中,我们将使用 @InjectMocks 把mock的 wordMap 注入到 MyDictionary dic 中:

java 复制代码
@Mock
Map<String, String> wordMap;

@InjectMocks
MyDictionary dic = new MyDictionary();

@Test
public void whenUseInjectMocksAnnotation_thenCorrect() {
    Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");

    assertEquals("aMeaning", dic.getMeaning("aWord"));
}

下面是 MyDictionary 类:

java 复制代码
public class MyDictionary {
    Map<String, String> wordMap;

    public MyDictionary() {
        wordMap = new HashMap<String, String>();
    }
    public void add(final String word, final String meaning) {
        wordMap.put(word, meaning);
    }
    public String getMeaning(final String word) {
        return wordMap.get(word);
    }
}

将Mock注入Spy中

与前面测试类似,我们可能想在spy中注入一个mock:

java 复制代码
@Mock
Map<String, String> wordMap;

@Spy
MyDictionary spyDic = new MyDictionary();

然而,Mockito 并不支持将mock注入spy,因此下面的测试会出现异常:

java 复制代码
@Test 
public void whenUseInjectMocksAnnotation_thenCorrect() { 
    Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning"); 

    assertEquals("aMeaning", spyDic.getMeaning("aWord")); 
}

如果我们想在 spy 中使用 mock,可以通过构造函数手动注入 mock:

java 复制代码
MyDictionary(Map<String, String> wordMap) {
    this.wordMap = wordMap;
}

现在需要我们手动创建spy,而不使用注释:

java 复制代码
@Mock
Map<String, String> wordMap; 

MyDictionary spyDic;

@BeforeEach
public void init() {
    MockitoAnnotations.openMocks(this);
    spyDic = Mockito.spy(new MyDictionary(wordMap));
}

现在测试将通过。

使用注解时遇到空指针

通常,当我们使用 @Mock 或 @Spy 注解时,可能会遇到 NullPointerException 异常:

java 复制代码
public class MockitoAnnotationsUninitializedUnitTest {

    @Mock
    List<String> mockedList;

    @Test(expected = NullPointerException.class)
    public void whenMockitoAnnotationsUninitialized_thenNPEThrown() {
        Mockito.when(mockedList.size()).thenReturn(1);
    }
}

大多数情况下,是因为我们没有启用 Mockito 注解。所以请查看我们第一节的内容,使用Mockito前别忘了先初始化。

备注

最后,这里有一些关于 Mockito 注解的说明:

  • Mockito 的注解减少了重复的mock代码
  • 它们使测试更具可读性。
  • @InjectMocks 是注入 @Spy 和 @Mock 实例所必需的。
  1. 参数匹配器 (matchers)
    Mockito以自然的java风格来验证参数值: 使用equals()函数。有时,当需要额外的灵活性时你可能需要使用参数匹配器,也就是argument matchers :

//stubbing using built-in anyInt() argument matcher

// 使用内置的anyInt()参数匹配器

when(mockedList.get(anyInt())).thenReturn("element");

//stubbing using custom matcher (let's say isValid() returns your own matcher implementation):

// 使用自定义的参数匹配器( 在isValid()函数中返回你自己的匹配器实现 )

when(mockedList.contains(argThat(isValid()))).thenReturn("element");

//following prints "element"

// 输出element

System.out.println(mockedList.get(999));

//you can also verify using an argument matcher

// 你也可以验证参数匹配器

verify(mockedList).get(anyInt());

参数匹配器使验证和测试桩变得更灵活。点击这里查看更多内置的匹配器以及自定义参数匹配器或者hamcrest 匹配器的示例。

如果仅仅是获取自定义参数匹配器的信息,查看ArgumentMatcher类文档即可。

为了合理的使用复杂的参数匹配,使用equals()与anyX() 的匹配器会使得测试代码更简洁、简单。有时,会迫使你重构代码以使用equals()匹配或者实现equals()函数来帮助你进行测试。

同时建议你阅读第15章节或者ArgumentCaptor类文档。ArgumentCaptor是一个能够捕获参数值的特俗参数匹配器。

参数匹配器的注意点 :

如果你使用参数匹配器,所有参数都必须由匹配器提供。

示例 : ( 该示例展示了如何多次应用于测试桩函数的验证 )

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));

//above is correct - eq() is also an argument matcher

// 上述代码是正确的,因为eq()也是一个参数匹配器

verify(mock).someMethod(anyInt(), anyString(), "third argument");

//above is incorrect - exception will be thrown because third argument

// 上述代码是错误的,因为所有参数必须由匹配器提供,而参数"third argument"并非由参数匹配器提供,因此的缘故会抛出异常

像anyObject(), eq()这样的匹配器函数不会返回匹配器。它们会在内部将匹配器记录到一个栈当中,并且返回一个假的值,通常为null。这样的实现是由于被Java编译器强加的静态类型安全。结果就是你不能在验证或者测试桩函数之外使用anyObject(), eq()函数。

  1. 验证函数的确切、最少、从未调用次数
    //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默认验证的就是times(1)

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)

// 使用never()进行验证,never相当于times(0)

verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()

// 使用atLeast()/atMost()

verify(mockedList, atLeastOnce()).add("three times");

verify(mockedList, atLeast(2)).add("five times");

verify(mockedList, atMost(5)).add("three times");

verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。

  1. 为返回值为void的函数通过Stub抛出异常
    doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:

// 调用这句代码会抛出异常

mockedList.clear();

关于doThrow|doAnswer 等函数族的信息请阅读第十二章节。

最初,stubVoid(Object) 函数用于为无返回值的函数打桩。现在stubVoid()函数已经过时,doThrow(Throwable)成为了它的继承者。这是为了提升与 doAnswer(Answer) 函数族的可读性与一致性。

  1. 验证执行执行顺序
    // A. Single mock whose methods must be invoked in a particular order
    // A. 验证mock一个对象的函数执行顺序
    List singleMock = mock(List.class);

//using a single mock

singleMock.add("was added first");

singleMock.add("was added second");

//create an inOrder verifier for a single mock

// 为该mock对象创建一个inOrder对象

InOrder inOrder = inOrder(singleMock);

//following will make sure that add is first called with "was added first, then with "was added second"

// 确保add函数首先执行的是add("was added first"),然后才是add("was added second")

inOrder.verify(singleMock).add("was added first");

inOrder.verify(singleMock).add("was added second");

// B. Multiple mocks that must be used in a particular order

// B .验证多个mock对象的函数执行顺序

List firstMock = mock(List.class);

List secondMock = mock(List.class);

//using mocks

firstMock.add("was called first");

secondMock.add("was called second");

//create inOrder object passing any mocks that need to be verified in order

// 为这两个Mock对象创建inOrder对象

InOrder inOrder = inOrder(firstMock, secondMock);

//following will make sure that firstMock was called before secondMock

// 验证它们的执行顺序

inOrder.verify(firstMock).add("was called first");

inOrder.verify(secondMock).add("was called second");

// Oh, and A + B can be mixed together at will

验证执行顺序是非常灵活的-你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可。 另外,你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。

  1. 确保交互(interaction)操作不会执行在mock对象上
    //using mocks - only mockOne is interacted
    // 使用Mock对象
    mockOne.add("one");

//ordinary verification

// 普通验证

verify(mockOne).add("one");

//verify that method was never called on a mock

// 验证某个交互是否从未被执行

verify(mockOne, never()).add("two");

//verify that other mocks were not interacted

// 验证mock对象没有交互过

verifyZeroInteractions(mockTwo, mockThree);

  1. 查找冗余的调用
    //using mocks
    mockedList.add("one");
    mockedList.add("two");

verify(mockedList).add("one");

//following verification will fail

// 下面的验证将会失败

verifyNoMoreInteractions(mockedList);

一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用。verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时。滥用它将导致测试代码的可维护性降低。你可以阅读这篇文档来了解更多相关信息。

never()是一种更为明显且易于理解的形式。

  1. 简化mock对象的创建

    最小化重复的创建代码

    使测试类的代码可读性更高

    使验证错误更易于阅读,因为字段名可用于标识mock对象

    public class ArticleManagerTest {

    @Mock private ArticleCalculator calculator;

    @Mock private ArticleDatabase database;

    @Mock private UserProvider userProvider;

    private ArticleManager manager;

    注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:

MockitoAnnotations.initMocks(testClass);

你可以使用内置的runner: MockitoJUnitRunner runner 或者一个rule : MockitoRule。 关于mock注解的更多信息可以阅读MockitoAnnotations文档。

  1. 为连续的调用做测试桩 (stub)
    有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。典型的运用就是使用mock迭代器。 原始版本的Mockito并没有这个特性,例如,可以使用Iterable或者简单的集合来替换迭代器。这些方法提供了更自然的方式,在一些场景中为连续的调用做测试桩会很有用。示例如下 :

when(mock.someMethod("some arg"))

.thenThrow(new RuntimeException())

.thenReturn("foo");

//First call: throws runtime exception:

// 第一次调用 : 抛出运行时异常

mock.someMethod("some arg");

//Second call: prints "foo"

// 第二次调用 : 输出"foo"

System.out.println(mock.someMethod("some arg"));

//Any consecutive call: prints "foo" as well (last stubbing wins).

// 后续调用 : 也是输出"foo"

System.out.println(mock.someMethod("some arg"));

另外,连续调用的另一种更简短的版本 :

// 第一次调用时返回"one",第二次返回"two",第三次返回"three"

when(mock.someMethod("some arg"))

.thenReturn("one", "two", "three");

  1. 为回调做测试桩
    Allows stubbing with generic Answer interface. 运行为泛型接口Answer打桩。

在最初的Mockito里也没有这个具有争议性的特性。我们建议使用thenReturn() 或thenThrow()来打桩。这两种方法足够用于测试或者测试驱动开发。

when(mock.someMethod(anyString())).thenAnswer(new Answer() {

Object answer(InvocationOnMock invocation) {

Object[] args = invocation.getArguments();

Object mock = invocation.getMock();

return "called with arguments: " + args;

}

});

//Following prints "called with arguments: foo"

// 输出 : "called with arguments: foo"

System.out.println(mock.someMethod("foo"));

  1. doReturn()、doThrow()、doAnswer()、doNothing()、doCallRealMethod()系列方法的运用
    通过when(Object)为无返回值的函数打桩有不同的方法,因为编译器不喜欢void函数在括号内...

使用doThrow(Throwable) 替换stubVoid(Object)来为void函数打桩是为了与doAnswer()等函数族保持一致性。

当你想为void函数打桩时使用含有一个exception 参数的doAnswer() :

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:

// 下面的代码会抛出异常

mockedList.clear();

当你调用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 这些函数时可以在适当的位置调用when()函数. 当你需要下面这些功能时这是必须的:

测试void函数

在受监控的对象上测试函数

不知一次的测试为同一个函数,在测试过程中改变mock对象的行为。

但是在调用when()函数时你可以选择是否调用这些上述这些函数。

阅读更多关于这些方法的信息:

doReturn(Object)

doThrow(Throwable)

doThrow(Class)

doAnswer(Answer)

doNothing()

doCallRealMethod()

  1. 监控真实对象
    你可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码。

监控一个真实的对象可以与"局部mock对象"概念结合起来。在1.8之前,mockito的监控功能并不是真正的局部mock对象。原因是我们认为局部mock对象的实现方式并不好,在某些时候我发现一些使用局部mock对象的合法用例。(第三方接口、临时重构遗留代码,完整的文章在这里 )

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对象调用真实对象的函数

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

// 因为size()函数被打桩了,因此这里返回的是100

System.out.println(spy.size());

//optionally, you can verify

// 交互验证

verify(spy).add("one");

verify(spy).add("two");

理解监控真实对象非常重要!

有时,在监控对象上使用when(Object)来进行打桩是不可能或者不切实际的。因此,当使用监控对象时请考虑doReturn|Answer|Throw()函数族来进行打桩。例如 :

List list = new LinkedList();

List spy = spy(list);

//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)

// 不可能 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生IndexOutOfBoundsException异常,因为真实List对象是空的

when(spy.get(0)).thenReturn("foo");

//You have to use doReturn() for stubbing

// 你需要使用doReturn()来打桩

doReturn("foo").when(spy).get(0);

Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。

因此结论就是 : 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。

相关推荐
oscar99916 天前
透彻理解并解决Mockito模拟框架的单元测试无法运行的问题
单元测试·mockito
oscar99922 天前
Java 单元测试模拟框架-Mockito 的介绍
java·开发语言·单元测试·mockito
lianghyan1 个月前
Junit test with mock
junit·mockito·spy
又菜又爱玩的晴晴2 个月前
mockito+junit完成单元测试
junit·单元测试·mockito
AaronJonah2 个月前
Junit + Mockito保姆级集成测试实践
spring boot·junit·集成测试·mockito
金斗潼关3 个月前
使用Mockito进行单元测试
单元测试·白盒测试·mockito
Mr.朱鹏4 个月前
MockIto插桩常规单元测试
java·spring·java-ee·kafka·单元测试·springboot·mockito
言午夏5 个月前
junit mockito Base基类
junit·mockito
胡玉洋5 个月前
单元测试实施最佳方案(背景、实施、覆盖率统计)
junit·单元测试·测试·mockito·jacoco·单元测试覆盖率·单测