Skip to main content

Mocktio的使用

huhxAbout 5 min

在编写Java代码,特别是Spring项目的时候,层级之间可能存在依赖,例如Service可能依赖Repository等等。所以当我们编写Service测试时,就可能需要mock或者verify Repository的行为,这个时候Mocktio库就派上用场了。

基础使用

首先如果在项目中使用Mocktio,需要引入相关依赖,以Gradle构建工具为例,在build.gradle中

testImplementation 'org.mockito:mockito-core:{version}'

当然如果是使用SpringBoot,默认引入的starter-test中就已经包含了mocktio-core,就不需要再显示的添加mocktio-core依赖了。

testImplementation 'org.springframework.boot:spring-boot-starter-test'

Mocktio有很多的注解可供我们使用,常见的有@Mock@Spy@InjectMocks。这些注解可以帮助我们在测试中创建模拟对象、注入模拟对象和捕获方法参数等,在一定程度上提高测试的可读性和可维护性,并简化测试代码的编写。

Enable Mocktio的注解

一般在测试项目中,我们在需要用到mocktio测试的class上面添加以下注解,它的作用是在测试中启用Mockito注解的支持。

@ExtendWith(MockitoExtension.class)

如果是Junit4,那么使用@RunWith(MockitoJUnitRunner.class)注解。如果是JUnit5,则选择@ExtendWith(MockitoExtension.class)

这样在测试中我们就可以使用mocktio的注解了,例如

@ExtendWith(MockitoExtension.class)
class TeamAppServiceTest {
    @Mock
    private TeamRepository teamRepository;

    @Mock
    private TeamProperties properties;

    @InjectMocks
    private TeamAppService teamAppService;

    @Test
    void should_return_response_with_id_given_requested_team_not_existed() {
        var createTeamRequest = buildCreateTeamRequest();
        when(teamRepository.findByName(TEAM_NAME)).thenReturn(Optional.empty());
        when(teamRepository.save(any(Team.class))).thenReturn(buildTeam());
        when(properties.environment()).thenReturn(ENVIRONMENT);

        var result = teamAppService.create(createTeamRequest);

        assertThat(result.getId()).isEqualTo(ID);
    }
}

常见的mocktio用法

我们创建一个名为MyList的类作为示例来说明mocktio的用法

public class MyList extends AbstractList<String> {
    @Override
    public String get(final int index) {
        return null;
    }

    @Override
    public int size() {
        return 1;
    }
}
  • mock对象方法的返回
var listMock = mock(MyList.class);
when(listMock.add(anyString())).thenReturn(false);

boolean added = listMock.add("any string");

assertThat(added).isFalse();
  • mock对象方法返回的另一种写法
var listMock = mock(MyList.class);
doReturn(false).when(listMock).add(anyString());

boolean added = listMock.add(randomAlphabetic(6));

assertThat(added).isFalse();

至于doReturn和thenReturn的区别,可以参考引用

  • mock对象方法抛出异常
var listMock = mock(MyList.class);
when(listMock.add(anyString())).thenThrow(IllegalStateException.class);

assertThrows(IllegalStateException.class, () -> listMock.add("any string"));
  • mock对象void方法抛出异常
var listMock = mock(MyList.class);
doThrow(NullPointerException.class).when(listMock).clear();

assertThrows(NullPointerException.class, () -> listMock.clear());

至于thenThrow和doThrow的区别,可以参考引用

  • mock对象方法多次调用
var listMock = mock(MyList.class);
when(listMock.add(anyString()))
  .thenReturn(false)
  .thenThrow(IllegalStateException.class);

assertThrows(IllegalStateException.class, () -> {
    listMock.add("any string 1");
    listMock.add("any string 2");
});
  • mock对象call真实的方法
var listMock = mock(MyList.class);
when(listMock.size()).thenCallRealMethod();

assertThat(listMock).hasSize(1); // MyList定义的size()方法的返回值是1

Mocktio之capture

当我们在想要测试某个方法被调用,而且想要verify方法参数内容的时候,ArgumentCaptor就派上用场了。它用于捕获和验证方法调用时传递的参数值,使用步骤如下

  • 创建ArgumentCaptor对象

使用ArgumentCaptor.forClass方法创建ArgumentCaptor对象,并指定要捕获的参数类型。

ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);

Mocktio还提供了一种使用注解的方式来创建ArgumentCaptor,所以上述的captor的创建可以使用

@Captor
private ArgumentCaptor<User> captor;

上述将创建一个用于捕获User对象的ArgumentCaptor

  • 设置方法调用和捕获参数

使用Mockito的when方法设置方法调用,并通过captor.capture()方法捕获参数值

userService.create(captor.capture());

上述将调用add方法并将传递的参数值捕获到captor对象中。

  • 验证参数值

使用ArgumentCaptor的getValue方法获取捕获的参数值,并进行相应的验证

assertThat(captor.getValue().getName()).isEqual("huhx");

下面是一个完整的例子,演示了ArgumentCaptor的使用。测试例子中,用的是junit5和assertj框架

code
@Data
@AllArgsConstructor
public class User {
    private String name;
}

@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping("/user")
    public void create(@RequestParam String name) {
        userService.create(new User(name));
    }
}

@Service
public class UserService {
    public void create(User user) {
        System.out.println(user);
    }
}

我们已经演示了如何在Mocktio中使用captor了,接下来我们给出一个比较复杂的例子:multiple captor

@PostMapping("/users")
public void createUsers() {
    userService.create(new User("huhx"));
    userService.create(new User("linux"));
}

在controller中会多次调用userService的create方法,并且每次参数还不一样。这个时候我们就可以使用captor的getAllValues方法。该方法用于获取捕获的所有参数值,返回值是List,其中包含了所有捕获的参数值的顺序。具体使用如下

@Test
void should_create_users() {
    userController.createUsers();

    verify(userService, times(2)).create(captor.capture());

    assertThat(captor.getAllValues().get(0).getName())
        .isEqualTo("huhx");
    assertThat(captor.getAllValues().get(1).getName())
        .isEqualTo("linux");
}

上述测试首先验证userService的create方法被调用两次,并且分别verify了两次调用参数的值。

Tips

getValue()返回的是参数列表中的最后一个值,getAllValues()返回的是整个参数列表。

Mocktio之静态mock

在编写测试时,我们经常会遇到需要模拟静态方法的情况。在Mockito 3.4.0 版本之前,无法直接模拟静态方法,只能借助于PowerMockito库。在稍后的版本,mocktio推出了支持静态方法mock的mocktio-inline库,让mock变得从未如此简单。

首先使用mocktio-inline替换之前的mocktio-core依赖,因为mocktio-inline已经依赖了mocktio-core

testImplementation 'org.mockito:mockito-inline:{version}'

关于如何使用,这边给出一个简单的示例

public Instant save() {
    return Instant.now();
}

@Test
void should_test_save() {
    try (MockedStatic<Instant> instantMockedStatic = mockStatic(Instant.class)) {
        instantMockedStatic.when(Instant::now).thenReturn(TIMESTAMP);

        var instant = userService.save();

        assertThat(instant).isEqualTo(TIMESTAMP);
    }
}

在上述示例中,我们使用mockStatic方法创建了一个MockedStatic对象,并通过when和thenReturn方法模拟了静态方法的返回值。

Tips

之所以使用try是因为只想静态的mock在try块中生效,在这之外Instant.now()的行为不再是mock的。MockedStatic实现了AutoCloseable接口,在try块的结束处,MockedStatic会有自动关闭的机制来清理静态mock。

方法分析

常见问题

doReturn和thenReturn的区别

doReturn和thenReturn的区别

thenThrow和doThrow

thenThrow和doThrow的区别