1、前言
之前写过一篇使用testMe自动生成单元测试用例,使用的是junit4来编写的单元测试用例,目前很多新项目都已经使用JDK11+以及SpringBoot3+。本次基于junit5+Mockito来编写单元测试。
2、Maven依赖
2.1 JDK21+SpringBoot版本基于3.1.0
SpringBoot依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
</exclusion>
</exclusions>
</dependency>
mockito依赖
<!--junit5单元测试-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.3.1</version>
</dependency>
lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>edge-SNAPSHOT</version>
</dependency>
2.2 JDK17+SpringBoot版本基于2.2.5.RELEASE
SpringBoot依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
Junit依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
</dependency>
mockito依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.2.0</version>
<exclusions>
<exclusion>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</exclusion>
<exclusion>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.1</version>
</dependency>
lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
3、业务代码
UserService代码
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Value("${custom.config.value:}")
private String configValue;
@Resource
private UserManager userManager;
@Override
public Long createUser(UserDto userDto) {
log.info("创建用户入参:{}", JSON.toJSONString(userDto));
String name = userDto.getUsername();
if (StringUtils.isBlank(name)) {
log.error("用户名称不能为空");
throw new BizException("用户名称不能为空");
}
Long id = userManager.createUser(userDto);
log.info("创建用户出参:{}", id);
return id;
}
@Override
public Boolean updateUser(UserDto userDto) {
log.info("更新用户入参:{}", JSON.toJSONString(userDto));
Long id = userDto.getId();
String name = userDto.getUsername();
if (Objects.isNull(id)) {
log.error("用户主键不能为空");
throw new BizException("用户主键不能为空");
}
if (StringUtils.isBlank(name)) {
log.error("用户名称不能为空");
throw new BizException("用户名称不能为空");
}
UserDto user = userManager.getUser(userDto);
if (Objects.isNull(user)) {
log.error("用户不存在");
throw new BizException("用户不存在");
}
Boolean result = userManager.updateUser(userDto);
log.info("更新用户出参:{}", result);
return result;
}
@Override
public UserDto getUser(UserDto userDto) {
log.info("获取用户入参:{}", JSON.toJSONString(userDto));
Long id = userDto.getId();
if (Objects.isNull(id)) {
log.error("用户主键不能为空");
throw new BizException("用户主键不能为空");
}
UserDto user = userManager.getUser(userDto);
log.info("获取用户出参:{}", user);
return user;
}
@Override
public Boolean batchCreateUser(List<UserDto> list) {
log.info("批量创建用户入参:{}", JSON.toJSONString(list));
if (CollectionUtils.isEmpty(list)) {
log.error("入参集合不能为空");
throw new BizException("入参集合不能为空");
}
int size = 10;
long keepAliveTime = 60;
long start = System.currentTimeMillis();
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10000);
ThreadFactory threadFactory = new DefaultThreadFactory("executor");
ExecutorService executorService
= new ThreadPoolExecutor(size, size, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
List<CompletableFuture<Boolean>> futureList = new ArrayList<>();
for (UserDto userDto : list) {
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
log.info("当前线程名称:{}", Thread.currentThread());
try {
Long id = userManager.createUser(userDto);
TimeUnit.SECONDS.sleep(3L);
log.info("线程:{} id={} done", Thread.currentThread(), id);
return Boolean.TRUE;
} catch (InterruptedException e) {
log.error("创建用户异常:{}", e.getMessage(), e);
return Boolean.FALSE;
}
}, executorService);
futureList.add(future);
}
Boolean result = Boolean.TRUE;
try {
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get(10, TimeUnit.SECONDS);
for (CompletableFuture<Boolean> future : futureList) {
Boolean back = future.get();
if (Boolean.FALSE.equals(back)) {
result = Boolean.FALSE;
}
}
} catch (Exception e) {
log.error("创建用户异常:{}", e.getMessage(), e);
result = Boolean.FALSE;
}
long end = System.currentTimeMillis();
log.info("批量创建用户耗时:{}", (end - start));
log.info("批量创建用户出参:{}", result);
return result;
}
}
UserManager 代码
@Slf4j
@Component
public class UserManagerImpl implements UserManager {
@Resource
private UserRepository userRepository;
@Override
public Long createUser(UserDto userDto) {
Long result = userRepository.createUser(userDto);
log.info("UserManagerImpl.createUser方法执行结果:{}", result);
return result;
}
@Override
public Boolean updateUser(UserDto userDto) {
return userRepository.updateUser(userDto);
}
@Override
public UserDto getUser(UserDto userDto) {
return userRepository.getUser(userDto);
}
}
UserRepository代码
@Slf4j
@Component
public class UserRepositoryImpl implements UserRepository {
@Override
public Long createUser(UserDto userDto) {
log.info("com.summer.toolkit.mock.UserRepositoryImpl.createUser方法执行");
return 1L;
}
@Override
public Boolean updateUser(UserDto userDto) {
log.info("com.summer.toolkit.mock.UserRepositoryImpl.updateUser方法执行");
return Boolean.TRUE;
}
@Override
public UserDto getUser(UserDto userDto) {
log.info("com.summer.toolkit.mock.UserRepositoryImpl.getUser方法执行");
return userDto;
}
4、单元测试
package com.summer.toolkit.service;
import com.alibaba.fastjson.JSON;
import com.summer.toolkit.dto.UserDto;
import com.summer.toolkit.exception.BizException;
import com.summer.toolkit.mock.UserManagerImpl;
import com.summer.toolkit.mock.UserRepositoryImpl;
import com.summer.toolkit.mock.UserServiceImpl;
import com.summer.toolkit.util.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@Slf4j
@ExtendWith(MockitoExtension.class)
@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
public class UserServiceTest {
@Mock
private UserRepositoryImpl userRepository;
@Spy
private UserManagerImpl userManager;
@InjectMocks
private UserServiceImpl userService;
@Captor
private ArgumentCaptor<UserDto> userDtoArgumentCaptor;
@BeforeEach
public void before() {
// 通过ReflectionTestUtils注入需要的非public字段数据
ReflectionTestUtils.setField(userManager, "userRepository", userRepository);
// 通过反射给@Value从配置文件注入的属性赋值
ReflectionTestUtils.setField(userService, "configValue", "definedValue");
}
@Test
public void testCreateUser() {
// 模拟依赖方法
//Mockito.when(userManager.createUser(any())).thenReturn(Long.valueOf(1));
Mockito.when(userRepository.createUser(any())).thenReturn(Long.valueOf(2));
// 调用被测方法
UserDto userDto = this.buildUserDto();
Long result = userService.createUser(userDto);
// 验证方法结果
Long expect = 2L;
Assertions.assertEquals(expect, result);
// 验证方法是否被调用,并带有超时时间
Mockito.verify(userManager, Mockito.timeout(100).times(1)).createUser(userDto);
// 自定义异常描述
Mockito.verify(userManager, Mockito.times(1).description("someMethod should be called twice")).createUser(any());
}
@Test
public void testCreateUserException() {
// 模拟依赖方法
Mockito.when(userRepository.createUser(any())).thenThrow(new BizException("defined message"));
// 验证方法是否抛出指定异常
BizException exception = Assertions.assertThrows(BizException.class, () -> {
// 调用被测方法
UserDto userDto = this.buildUserDto();
userService.createUser(userDto);
});
Assertions.assertEquals("defined message", exception.getMessage());
}
@Test
public void testCreateUserContinuity() {
// 模拟依赖方法多次调用返回值
Mockito.when(userRepository.createUser(any())).thenReturn(1L, 2L, 3L);
// 多次调用被测方法并验证每次返回值是否符合预期
UserDto userDto = this.buildUserDto();
Long result = userService.createUser(userDto);
Assertions.assertEquals(1L, result);
result = userService.createUser(userDto);
Assertions.assertEquals(2L, result);
result = userService.createUser(userDto);
Assertions.assertEquals(3L, result);
// 验证方法是否被调用
Mockito.verify(userManager, Mockito.timeout(100).times(3)).createUser(userDto);
// 自定义异常描述
Mockito.verify(userManager, Mockito.times(3).description("someMethod should be called twice")).createUser(any());
}
@Test
public void testUpdateUser() {
// 模拟依赖方法
Mockito.when(userManager.updateUser(any())).thenReturn(Boolean.TRUE);
Mockito.when(userManager.getUser(any())).thenReturn(new UserDto());
// 调用被测方法
UserDto userDto = this.buildUserDto();
userDto.setId(1L);
Boolean result = userService.updateUser(userDto);
// 验证方法结果
Assertions.assertEquals(Boolean.TRUE, result);
// 验证方法是否被调用
Mockito.verify(userManager).getUser(any());
Mockito.verify(userManager).updateUser(any());
}
@Test
public void testGetUser() {
// 模拟依赖方法
Mockito.when(userManager.getUser(any())).thenReturn(new UserDto());
// 调用被测方法
UserDto userDto = this.buildUserDto();
userDto.setId(1L);
UserDto result = userService.getUser(userDto);
// 验证方法结果
Assertions.assertNotNull(result);
// 验证方法是否被调用
Mockito.verify(userManager).getUser(userDto);
}
@Test
public void testBatchCreateUserException() {
// 模拟依赖方法,指定单个异常类型
Mockito.when(userManager.createUser(any())).thenThrow(BizException.class);
// 调用被测方法
List<UserDto> param = new ArrayList<>();
UserDto userDto = this.buildUserDto();
param.add(userDto);
Boolean result = userService.batchCreateUser(param);
// 验证方法结果
Assertions.assertEquals(Boolean.FALSE, result);
// 验证方法是否被调用,默认一次
Mockito.verify(userManager).createUser(userDto);
// 验证方法是否被调用了1次
Mockito.verify(userManager, Mockito.times(1)).createUser(any());
}
@Test
public void testBatchCreateUserTimes() {
// 模拟依赖方法
Mockito.when(userManager.createUser(any())).thenReturn(1L);
// 调用被测方法
List<UserDto> param = new ArrayList<>();
UserDto userDto = this.buildUserDto();
param.add(userDto);
param.add(userDto);
param.add(userDto);
Boolean result = userService.batchCreateUser(param);
// 验证方法结果
Assertions.assertEquals(Boolean.TRUE, result);
// 验证方法是否被调用了3次
Mockito.verify(userManager, Mockito.times(3)).createUser(any());
Mockito.verify(userManager, Mockito.atLeast(3)).createUser(any());
Mockito.verify(userManager, Mockito.atMost(3)).createUser(any());
}
@Test
public void testBatchCreateUserCallBack() {
// 模拟依赖方法,指定回调函数,回调函数返回值会覆盖原有逻辑的真实返回值
Mockito.when(userManager.createUser(any())).thenAnswer(new Answer<Long>() {
@Override
public Long answer(InvocationOnMock invocationOnMock) throws Throwable {
log.info("执行回调方法");
Object[] arguments = invocationOnMock.getArguments();
log.info("回调参数为:{}", JSON.toJSONString(arguments));
Method method = invocationOnMock.getMethod();
log.info("方法名称为:{}", method.getName());
return 2L;
}
});
// 调用被测方法
UserDto userDto = this.buildUserDto();
Long result = userService.createUser(userDto);
// 验证方法结果,此结果为回调函数的返回值
Long expect = 2L;
Assertions.assertEquals(expect, result);
}
@Test
public void testCaptor() {
// 模拟依赖方法
Mockito.when(userManager.createUser(any())).thenReturn(1L);
// 调用被测方法
UserDto userDto = this.buildUserDto();
Long result = userService.createUser(userDto);
// 验证方法结果
Assertions.assertEquals(1L, result);
// 捕获调用方法参数,验证保存用户时数据主键是否为空
Mockito.verify(userManager).createUser(userDtoArgumentCaptor.capture());
Assertions.assertNull(userDtoArgumentCaptor.getValue().getId());
}
@Test
@DisplayName("静态方法测试")
public void testFileUtils() {
// 构建对象
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
// 模拟对应的静态类
// JDK11及以上版本中,try块中的变量可以在外部声明
MockedStatic<FileUtils> mocked = Mockito.mockStatic(FileUtils.class);
try (mocked) {
// 模拟依赖静态方法
mocked.when(() -> FileUtils.readFileAllLines(anyString())).thenReturn(list);
// 调用被测方法
List<String> lines = FileUtils.readFileAllLines(anyString());
// 验证方法结果
Assertions.assertEquals(list.size(), lines.size());
} catch (Exception e) {
log.error("模拟静态方法异常:{}", e.getMessage(), e);
}
}
/**
* 构建用户数据传输对象
*
* @return UserDto 返回构建好的用户数据传输对象
*/
private UserDto buildUserDto() {
UserDto userDto = new UserDto();
userDto.setUsername("小明");
userDto.setBirthday(new Date());
userDto.setAddress("北京市大兴区亦庄经济开发区");
userDto.setComment("加麻加辣");
userDto.setGender(1);
return userDto;
}
}