Mockito + Spring集成测试用例相互污染问题以及解决方案

阅读说明:

本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。

在上一篇文章 单元测试神器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的上下文还是被污染了。

本文推出优化框架,解决了上述问题,该框架有以下优点:

  1. 简化使用流程,封装了通用操作。
  2. 安全可靠。不使用Mockito注解,减少对Spring容器的侵入。
  3. 解决上下文污染问题,在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);
    }
相关推荐
扎Zn了老Fe14 天前
单元测试神器:Mockito
后端·mockito
十连满潜1 个月前
springboot集成mockito和jacoco实践
后端·单元测试·mockito
Uranus^2 个月前
深入解析Spring Boot与JUnit 5集成测试的最佳实践
spring boot·单元测试·集成测试·junit 5·mockito
Uranus^2 个月前
深入解析Spring Boot与JUnit 5的集成测试实践
spring boot·单元测试·集成测试·junit 5·mockito
杨凯凡4 个月前
Mockito 全面指南:从单元测试基础到高级模拟技术
java·单元测试·mockito
程序视点4 个月前
【肝】单元测试一篇汇总!开发人员必学!
前端·单元测试·mockito
oscar9998 个月前
透彻理解并解决Mockito模拟框架的单元测试无法运行的问题
单元测试·mockito
oscar9998 个月前
Java 单元测试模拟框架-Mockito 的介绍
java·开发语言·单元测试·mockito
lianghyan9 个月前
Junit test with mock
junit·mockito·spy