阅读说明:
本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。
在上一篇文章 单元测试神器Mockito,一网打尽用法 中,已经讲解了Mockito的用法,本文继续深入讲解Spring中的使用。在Spring框架中,使用Mockito进行单元测试时,我们可以利用一些注解来简化模拟对象的创建和注入。主要的注解有:
@Mock
:用于创建模拟对象。
@Spy
:用于创建部分模拟的对象(即真实对象,但可以模拟某些方法)。
@InjectMocks
:用于创建被测试类的实例,并将@Mock
或@Spy
注解创建的模拟对象注入到该实例中。
@MockBean:SpringBoot独有。将Mock对象添加到Spring容器,替换掉替换容器中同类型的真实Bean。当然,也能手动完成。在要被注入的对象上添加双注解:@InjectMocks
@Autowired,那么mock的对象也将注入Spring容器中,替换原有对象。
@SpyBean:SpringBoot独有。将Spy对象添加到Spring容器,替换掉替换容器中同类型的真实Bean。
单元测试示例(两种场景)
单元测试使用注解方式时,按照以下流程:
1.MockitoAnnotations.openMocks启用Mockito注解;
2.创建mock对象和设置对应mock返回,执行单元测试用例;
3.运行结束后, Mockito.reset重置mock对象,防止测试用例之间发生干扰。
纯Mockito测试(不启动Spring容器)
当不需要启动Spring容器时,使用注解@ExtendWith(MockitoExtension.class)
(JUnit5)或者使用@RunWith(MockitoJUnitRunner.class)
(JUnit4)来开启单元测试。
typescript
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
// 被测试的Service
class PaymentService {
private final UserService userService;
private final OrderService orderService;
public PaymentService(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
public String processPayment(long userId, String orderId) {
if (userService.isUserActive(userId)) {
return orderService.finalizeOrder(orderId);
}
return "User is inactive";
}
}
// 依赖的Service接口
interface UserService {
boolean isUserActive(long userId);
}
interface OrderService {
String finalizeOrder(String orderId);
}
// 测试类
class PaymentServiceTest {
@Mock
private UserService userService;
@Mock
private OrderService orderService;
@InjectMocks // 自动注入上面的Mock对象
private PaymentService paymentService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this); // 初始化注解
}
@AfterEach
void reset() {
Mockito.reset(userService, orderService);
}
@Test
void testProcessPayment_Success() {
// 1. 设置Mock行为
when(userService.isUserActive(1001L)).thenReturn(true);
when(orderService.finalizeOrder("ORD-123")).thenReturn("Payment Successful");
// 2. 调用被测试方法
String result = paymentService.processPayment(1001L, "ORD-123");
// 3. 验证结果和交互
assertEquals("Payment Successful", result);
verify(userService).isUserActive(1001L); // 验证方法调用
verify(orderService).finalizeOrder("ORD-123");
}
@Test
void testProcessPayment_UserInactive() {
when(userService.isUserActive(1002L)).thenReturn(false);
String result = paymentService.processPayment(1002L, "ORD-456");
assertEquals("User is inactive", result);
verify(orderService, never()).finalizeOrder(any()); // 验证方法未被调用
}
}
Spring集成测试
当需要启用Spring容器时,这也是项目中经常要用到的场景。@MockBean
(由Spring提供)来模拟Bean。
typescript
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 启动Spring容器
class PaymentServiceIntegrationTest {
@MockBean // 替换Spring容器中的Bean
private UserService userService;
@MockBean
private OrderService orderService;
@Autowired // 注入真实的PaymentService(依赖已自动替换为Mock)
private PaymentService paymentService;
@Test
void testIntegration_Success() {
when(userService.isUserActive(2001L)).thenReturn(true);
when(orderService.finalizeOrder("ORD-789")).thenReturn("SUCCESS");
String result = paymentService.processPayment(2001L, "ORD-789");
assertEquals("SUCCESS", result);
verify(orderService).finalizeOrder("ORD-789");
}
}
基于Mockito + Spring集成测试优化框架
如果你使用比较多,就能发现一个问题。被mock的对象是一个装饰过的对象,Mockito.reset是把mock的方法给重置了, Spring中依然是这个mock对象,并非Spring中原有的对象。所以实际上,Spring的上下文还是被污染了。
本文推出优化框架,解决了上述问题,该框架有以下优点:
- 简化使用流程,封装了通用操作。
- 安全可靠。不使用Mockito注解,减少对Spring容器的侵入。
- 解决上下文污染问题,在Spring容器注入mock前将被替代的对象保存好,单元测试结束时,将被替代的对象重新注入Spring容器。
框架简化核心代码如下:
scss
@SpringBootTest(classes = TestApplication.class)
public abstract class CustomTestFramework {
private final LogWriter logWriter = LogFactory.getLogger(getClass());
@Autowired
private AppContext appContext;
/** 重置处理器列表 */
protected final List<TestCleanupHandler> cleanupHandlers = new ArrayList<>();
protected void preTestSetup() throws Exception {}
protected void postTestCleanup() throws Exception {}
@BeforeEach
public void initializeTest() throws Exception {
activateServiceMocks();
preTestSetup();
}
@AfterEach
public void finalizeTest() throws Exception {
// 执行注册的清理任务
for (TestCleanupHandler handler : cleanupHandlers) {
handler.executeCleanup();
}
postTestCleanup();
}
protected void activateServiceMocks(){
try{
prepareServiceMocks();
} catch (Exception ex) {
logWriter.error("激活服务模拟异常, 类:{}", getClass(), ex);
}
}
private void prepareServiceMocks() throws Exception {
configureMockDependencies(getClass());
}
private void configureMockDependencies(Class<?> targetClass) throws IllegalAccessException {
if (targetClass.getSuperclass() != null) {
configureMockDependencies(targetClass.getSuperclass());
}
Field[] classFields = targetClass.getDeclaredFields();
for (Field field : classFields) {
ServiceMock annotation = field.getAnnotation(ServiceMock.class);
if(annotation == null){
continue;
}
Class<?> serviceInterface = field.getType();
field.setAccessible(true);
Object serviceMockInstance;
Object existingMock = field.get(this);
if(existingMock != null){
serviceMockInstance = existingMock;
} else {
serviceMockInstance = Mockito.mock(serviceInterface);
field.set(this, serviceMockInstance);
}
Class<?>[] dependentServices = annotation.value();
for (Class<?> dependency : dependentServices) {
Object serviceBean = appContext.getBean(dependency);
Object originalImplementation = this.retrieveOriginalInstanceFromProxy(serviceBean);
if (originalImplementation == null) {
logWriter.error("@ServiceMock无法定位原始实现:{}, 上下文中的Bean:{}",
dependency, serviceBean);
continue;
}
Class<?> currentImplClass = originalImplementation.getClass();
boolean fieldFound = false;
while (!currentImplClass.equals(Object.class)) {
for (Field implField : currentImplClass.getDeclaredFields()) {
if (serviceInterface.isAssignableFrom(implField.getType())) {
implField.setAccessible(true);
Object originalService = implField.get(originalImplementation);
implField.set(originalImplementation, serviceMockInstance);
// 注册清理任务
this.registerCleanupTask(() -> {
try {
implField.set(originalImplementation, originalService);
}
catch (Exception e) {
logWriter.error("@ServiceMock恢复失败,依赖类:{}, 服务接口:{}",
dependency, serviceInterface, e);
}
});
fieldFound = true;
break;
}
}
if (fieldFound) {
break;
}
currentImplClass = currentImplClass.getSuperclass();
}
}
}
}
public void registerCleanupTask(TestCleanupHandler handler){
cleanupHandlers.add(handler);
}
@FunctionalInterface
public interface TestCleanupHandler {
void executeCleanup();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ServiceMock {
Class<?>[] value();
}
public static Object retrieveOriginalInstanceFromProxy(Object proxyBean) {
if (!ProxyUtils.isProxyObject(proxyBean)) {
return proxyBean;
}
if (ProxyUtils.isCglibProxy(proxyBean)) {
return extractTargetFromCglib(proxyBean);
}
return null;
}
private static Object extractTargetFromCglib(Object cglibProxy) {
try {
Field callbackField = cglibProxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
callbackField.setAccessible(true);
Object interceptorHandler = callbackField.get(cglibProxy);
Field advisedSupportField = interceptorHandler.getClass().getDeclaredField("advised");
advisedSupportField.setAccessible(true);
AdvisedSupport config = (AdvisedSupport) advisedSupportField.get(interceptorHandler);
return config.getTargetSource().obtainTarget();
} catch (ReflectiveOperationException | SecurityException e) {
handleReflectionIssue(e);
}
return null;
}
private static void handleReflectionIssue(Exception ex) {
ex.printStackTrace();
// 可添加额外处理逻辑如日志记录
}
}
使用示例:
scala
public class UserServiceTest extends CustomTestFramework {
@ServiceMock(UserRepository.class)
private UserDao userDao;
@Autowired
private UserService userService;
@Test
public void testGetUserById_Success() {
// 准备测试数据
Long userId = 1L;
User mockUser = new User(userId, "test@example.com");
// 定义模拟行为 - 当调用 userRepository.findById(1L) 时返回 mockUser
Mockito.when(userDao.findById(userId)).thenReturn(mockUser);
// 执行测试
User result = userService.getUserById(userId);
// 验证结果
assertNotNull(result);
assertEquals(userId, result.getId());
assertEquals("test@example.com", result.getEmail());
// 验证 userRepository.findById 被调用了一次
Mockito.verify(userDao, Mockito.times(1)).findById(userId);
}