Mocktio的使用
在编写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框架
@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);
}
}
// 下面是controller的测试
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Test
void should_create_user() {
var captor = ArgumentCaptor.forClass(User.class);
userController.create("huhx");
verify(userService).create(captor.capture());
assertThat(captor.getValue().getName())
.isEqualTo("huhx");
}
}
我们已经演示了如何在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的区别