抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Mockito 是一个功能强大的 Java 单元测试 Mocking 框架,本文将介绍 Mockito 的用法。

Mockito 快速入门

快速使用

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)。

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

启用 Mockito

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

  • 在 JUnit 上设置 MockitoJUnitRunner
@ExtendWith(MockitoExtension.class)
public class MockitoAnnotationUnitTest {
    ...
}
  • 调用 MockitoAnnotations.openMocks() 方法
@Before
public void init() {
    MockitoAnnotations.openMocks(this);
}
  • 使用 MockitoJUnit.rule()
public class MockitoAnnotationsInitWithMockitoJUnitRuleUnitTest {

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

    ...
}

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

@Mock 注解

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

@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());
}

mock 意思就是造一个假的模拟对象,不会去调用这个真正对象的方法,这个 mock 对象里的所有行为都是未定义的,属性也不会有值,需要你自己去定义它的行为。比如说,你可以 mock 一个假的 size() , 使其返回 100,但实际上并没有真的创建一个 size 为 100 的 Map

@DoNotMock 注解

@DoNotMock 注解用来标记不要mock的类或接口:

import org.mockito.exceptions.misusing.DoNotMock;

@DoNotMock(reason = "Use a real instance instead")
public abstract class NotToMock {
    // Class implementation
}

@Spy 注解

@Spy 注释用于创建一个真实对象并监视这个真实对象。@Spy 对象能够调用所监视对象的所有正常方法,同时仍然跟踪每一次交互,就像我们使用 mock 一样,可以自己定义行为。

@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());
}

mock 是模拟整个生成一个假对象,spy 像是间谍潜伏在真实对象里去篡改行为。

@Mock和@Spy的区别

  1. 在使用 @Mock 时,mockito 创建了类的一个基础套壳实例,完全用于跟踪与它的全部交互行为。这不是一个真正的对象,并且不维护状态,不存在更改。
  2. 当使用 @Spy 时,mockito 创建一个类的真实实例,可以跟踪与它的每个交互行为,这个真实类能维护类状态的变化。

@Captor

@Captor 注释用于创建 ArgumentCaptor 实例,该实例用于捕获方法参数值,来用于进一步做断言验证。mockito 使用参数类的 equals() 方法验证参数值是否相同。

不使用 @Captor 注解,手动创建一个 ArgumentCaptor:

@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:

@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

在 mockito 中,我们需要创建被测试的类对象,然后插入它的依赖项(mock)来完全测试行为。因此,我们要用到 @InjectMocks 注释。

@InjectMocks 标记了一个应该执行注入的字段。Mockito 会按照下面优先级通过构造函数注入、setter 注入或属性注入,来尝试注入你标识的 mock。如果上面三种任何给定的注入策略注入失败了,Mockito 不会报错

@InjectMocks 一般是你要测的类,他会把要测类的 mock 属性自动注入进去。@Mock 则是你要造假模拟的类。

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

@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 类:

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 函数的用法

  1. 简单 mock

mock 有好几个重载方法,最简单的一个只需传入被 mock 的 class:

public static <T> T mock(Class<T> classToMock)

使用此方法来mock一个类:

MyList listMock = mock(MyList.class);
  1. 指定 mock 的名字

mock 的第二个重载方法,第二个参数指定了 mock 的名称:

public static <T> T mock(Class<T> classToMock, String name)

一般来说,这个名字没啥用。不过,它在调试时可能会有所帮助,因为我们会使用 mock 的名字来追踪错误。

  1. 自定义Answer

在创建时配置 mock 对交互的 answer 的策略。 该方法的定义如下所示:

public static <T> T mock(Class<T> classToMock, Answer defaultAnswer)

Answer 接口的实现定义:

class CustomAnswer implements Answer<Boolean> {

    @Override
    public Boolean answer(InvocationOnMock invocation) throws Throwable {
        return false;
    }
}

使用上面的 CustomAnswer 类来生成mock:

MyList listMock = mock(MyList.class, new CustomAnswer());

MockSettings 如果我们不对方法设置期望值,CustomAnswer 类型配置的默认 answer 就会发挥作用。

  1. MockSettings

最后一个 mock 方法是带有 MockSettings 参数的重载方法。我们使用这个重载方法来提供一个非标准的 mock 。

MockSettings 接口的方法支持多种自定义设置,例如使用 invocationListeners 为当前 mock 上的方法调用注册监听器、使用 serializable 配置序列化、使用 spiedInstance 指定要监视的实例、使用 useConstructor 配置 Mockito 在实例化 mock 时尝试使用构造函数等。

MockSettings 对象由工厂方法实例化:

MockSettings customSettings = withSettings().defaultAnswer(new CustomAnswer());

在创建新的 mock 时使用该设置对象:

MyList listMock = mock(MyList.class, customSettings);

when/then 函数的用法

Stub 打桩
Mockito 中 when().thenReturn(); 这种语法来定义对象方法和参数(输入),然后在 thenReturn 中指定结果(输出)。此过程称为 Stub 打桩 。一旦这个方法被 stub 了,就会一直返回这个 stub 的值。

注意

  • 对于 static 和 final 方法, Mockito 无法对其 when(…).thenReturn(…) 操作。
  • 当我们连续两次为同一个方法使用 stub 的时候,他只会只用最新的一次。

MyList 类为例:

public class MyList extends AbstractList<String> {

    @Override
    public String get(final int index) {
        return null;
    }
    @Override
    public int size() {
        return 1;
    }
}
  1. when().thenReturn() 模拟方法的返回
MyList listMock = Mockito.mock(MyList.class);
when(listMock.add(anyString())).thenReturn(false);

boolean added = listMock.add(randomAlphabetic(6));
assertThat(added, is(false));
  1. doReturn().when() 模拟方法的返回
MyList listMock = Mockito.mock(MyList.class);
doReturn(false).when(listMock).add(anyString());

boolean added = listMock.add(randomAlphabetic(6));
assertThat(added, is(false));
  1. when().thenThrow() 模拟异常(方法返回类型非 void )
@Test(expected = IllegalStateException.class)
public void givenMethodIsConfiguredToThrowException_whenCallingMethod_thenExceptionIsThrown() {
    MyList listMock = Mockito.mock(MyList.class);
    when(listMock.add(anyString())).thenThrow(IllegalStateException.class);

    listMock.add(randomAlphabetic(6));
}
  1. doThrow().when() 模拟异常(方法返回类型为 void )
MyList listMock = Mockito.mock(MyList.class);
doThrow(NullPointerException.class).when(listMock).clear();

listMock.clear();
  1. 模拟方法的多次调用

第二次调用 add 方法会抛出 IllegalStateException

MyList listMock = Mockito.mock(MyList.class);
when(listMock.add(anyString()))
  .thenReturn(false)
  .thenThrow(IllegalStateException.class);

listMock.add(randomAlphabetic(6));
listMock.add(randomAlphabetic(6)); // will throw the exception
  1. thenCallRealMethod() 调用 mock 对象的真实方法
MyList listMock = Mockito.mock(MyList.class);
when(listMock.size()).thenCallRealMethod();

assertThat(listMock.size(), equalTo(1));
  1. doAnswer().when() 设置默认返回
MyList listMock = Mockito.mock(MyList.class);
doAnswer(invocation -> "Always the same").when(listMock).get(anyInt());

String element = listMock.get(1);
assertThat(element, is(equalTo("Always the same")));
  1. doNothing().when().notify() 跳过 void 方法
doNothing().when(obj).notify();
  1. when(obj).notify() 跳过 void 方法
when(obj).notify();

verify 函数的用法

Mockito Verify 方法用于检查是否发生了某些行为。我们可以在测试方法代码的末尾使用 Mockito 验证方法,以确保调用了指定的方法。

  1. 在模拟列表对象上仅调用一次add(“Pig”)
@Test
void test() {
	List<String> mockList = mock(List.class);
	mockList.add("Pig");
	mockList.size();
	verify(mockList).add("Pig");
}

与通过verify方法使用times(1)参数调用相同

verify(mockList, times(1)).size();
  1. 验证调用次数
verify(mockList, times(1)).size(); // 与常规验证方法相同
verify(mockList, atLeastOnce()).size(); // 至少调用1次
verify(mockList, atMost(2)).size(); // 最多调用2次
verify(mockList, atLeast(1)).size(); // 至少调用1次
verify(mockList, never()).clear(); // 永远不会被调用
  1. verifyNoMoreInteractions()

在所有验证方法之后可以使用此方法,以确保所有交互都得到验证。如果模拟对象上存在任何未验证的交互,它将使测试失败。

// 所有交互都经过验证,因此下面调用将通过
verifyNoMoreInteractions(mockList);
mockList.isEmpty();
// isEmpty() 没有经过验证, 所以下面调用将失败
verifyNoMoreInteractions(mockList);
  1. verifyZeroInteractions()

verifyZeroInteractions() 方法的行为与 verifyNoMoreInteractions() 方法相同。

Map mockMap = mock(Map.class);
Set mockSet = mock(Set.class);
verify(mockList).isEmpty();
verifyZeroInteractions(mockList, mockMap, mockSet);
  1. 验证仅方法调用

如果要验证仅调用了一个方法,则可以将 only() 与 verify 方法一起使用。

Map mockMap = mock(Map.class);
mockMap.isEmpty();
verify(mockMap, only()).isEmpty();
  1. 验证调用顺序

我们可以使用 InOrder 来验证调用顺序。我们可以跳过任何方法进行验证,但是要验证的方法必须以相同的顺序调用。

List<String> mockedList = mock(MyList.class);
mockedList.size();
mockedList.add("a parameter");
mockedList.clear();

InOrder inOrder = Mockito.inOrder(mockedList);
inOrder.verify(mockedList).size();
inOrder.verify(mockedList).add("a parameter");
inOrder.verify(mockedList).clear();

Argument Matcher

我们可以以多种方式配置 mocked 方法。一种选择是返回固定值:

doReturn("Flower").when(flowerService).analyze("poppy");

在上面的例子中,只有当 FlowerServiceanalyze 方法接收到字符串"poppy"时,才会返回字符串"Flower"。

然而,有时可能需要对更广泛的值或未知值做出响应。

在这种情况下,我们可以通过参数匹配器来配置我们的 mocked 方法:

when(flowerService.analyze(anyString())).thenReturn("Flower");

现在,由于使用了 anyString 参数匹配器,无论我们传递什么值给 analyze 方法,结果都会相同。ArgumentMatchers 使我们能够灵活地进行验证或模拟。

如果一个方法有多个参数,我们不能只对其中一些参数使用 ArgumentMatchers 。Mockito 要求我们为所有参数提供匹配器或确切的值。

以下是一个不正确的示例:

when(flowerService.isABigFlower("poppy", anyInt())).thenReturn(true);

要解决这个问题,并保持字符串名称"poppy",我们将使用 eq matcher:

when(flowerService.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

当我们使用匹配器时,还有两点需要注意:

  • 我们不能用它们作为返回值;在模拟调用时,我们需要确切的值。
  • 我们不能在验证或模拟之外使用参数匹配器

根据第二点,Mockito 会检测到参数放置不当,并抛出 InvalidUseOfMatchersException 异常。

一个不好的例子是:

flowerController.isAFlower("poppy");

String orMatcher = or(eq("poppy"), endsWith("y"));
assertThrows(InvalidUseOfMatchersException.class, () -> verify(flowerService).analyze(orMatcher));

定制 Argument Matcher

实现一个自定义参数匹配器:

public class MessageMatcher implements ArgumentMatcher<Message> {

    private Message left;

    // constructors

    @Override
    public boolean matches(Message right) {
        return left.getFrom().equals(right.getFrom()) &&
          left.getTo().equals(right.getTo()) &&
          left.getText().equals(right.getText()) &&
          right.getDate() != null &&
          right.getId() != null;
    }
}

使用我们的匹配器:

MessageDTO messageDTO = new MessageDTO();
messageDTO.setFrom("me");
messageDTO.setTo("you");
messageDTO.setText("Hello, you!");

messageController.createMessage(messageDTO);

Message message = new Message();
message.setFrom("me");
message.setTo("you");
message.setText("Hello, you!");

verify(messageService, times(1)).deliverMessage(argThat(new MessageMatcher(message)));

ArgumentCaptor

ArgumentCaptor 允许我们捕获传递给方法的参数以进行检查。当我们在测试中无法访问方法外部的参数时,这特别有用

例如,考虑一个名为 EmailService 的类,它有一个 send 方法,我们希望对其进行测试:

public class EmailService {

    private DeliveryPlatform platform;

    public EmailService(DeliveryPlatform platform) {
        this.platform = platform;
    }

    public void send(String to, String subject, String body, boolean html) {
        Format format = Format.TEXT_ONLY;
        if (html) {
            format = Format.HTML;
        }
        Email email = new Email(to, subject, body);
        email.setFormat(format);
        platform.deliver(email);
    }

    ...
}

EmailService.send 中,注意 platform.deliver 接受一个新的 Email 作为参数。在测试中,我们想检查新 Email 的 format 字段是否设置为 Format.HTML。为此,我们需要捕获并检查传递给 platform.deliver 的参数。

  1. 设置单元测试

创建我们的单元测试类:

@ExtendWith(MockitoExtension.class)
class EmailServiceUnitTest {

    @Mock
    DeliveryPlatform platform;

    @InjectMocks
    EmailService emailService;

    ...
}
  1. 添加 ArgumentCaptor 字段

添加一个新的Email类型的ArgumentCaptor字段来存储捕获的参数:

@Captor
ArgumentCaptor<Email> emailCaptor;
  1. 捕获参数

使用 verify()ArgumentCaptor 捕获 Email

verify(platform).deliver(emailCaptor.capture());

我们可以获取捕获的值,并将其存储为新的 Email 对象:

Email emailCaptorValue = emailCaptor.getValue();
  1. 检查捕获的值

带有断言来检查捕获的 Email 对象:

@Test
void whenDoesSupportHtml_expectHTMLEmailFormat() {
    String to = "[email protected]";
    String subject = "Using ArgumentCaptor";
    String body = "Hey, let'use ArgumentCaptor";

    emailService.send(to, subject, body, true);

    verify(platform).deliver(emailCaptor.capture());
    Email value = emailCaptor.getValue();
    assertThat(value.getFormat()).isEqualTo(Format.HTML);
}

评论