无需@SpringBootTest测试带参数校验的Spring Boot Service层Bean
解决方案
方案1:纯单元测试(无Spring上下文,速度最快)
Spring 中@Validated标注Bean的方法参数校验能力,本质是通过MethodValidationInterceptor切面实现的,我们可以手动为原生Service实例织入该切面,无需启动完整Spring应用上下文即可获得完整校验能力。
该方案和普通Mockito单元测试运行速度一致,完全兼容Spring Boot 2.5.2 + Java 11版本。
测试代码示例
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.aop.framework.ProxyFactory; import org.springframework.validation.beanvalidation.MethodValidationInterceptor; import javax.validation.ConstraintViolationException; import javax.validation.Validation; import javax.validation.ValidatorFactory; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; // 其余断言、验证相关导入按你原有写法即可 @ExtendWith(MockitoExtension.class) public class ThingServiceUnitTest { @Mock private ThingRepository repo; private ThingService service; @BeforeEach void initializeService() { // 1. 构造原生Service实例 ThingService targetService = new ThingService(repo); // 2. 构造默认校验器和校验拦截器 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); MethodValidationInterceptor validationAdvice = new MethodValidationInterceptor(validatorFactory.getValidator()); // 3. 为原生Service创建代理,织入校验拦截器 ProxyFactory proxyFactory = new ProxyFactory(targetService); proxyFactory.addAdvice(validationAdvice); // 4. 拿到代理后的Service实例,具备完整校验能力 service = (ThingService) proxyFactory.getProxy(); } // 原有测试用例完全无需修改,直接复用即可 @Test public void createSuccess() { when(repo.save(any(Thing.class))).thenAnswer(returnsFirstArg()); Thing thing = service.create(2L, new Thing(5L, "foo")); // 原有断言、验证逻辑 } @Test public void createNullArg() { ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> { service.create(null, new Thing(5L, "foo")); }); // 原有异常断言逻辑 } }
方案2:轻量Spring上下文测试
仅加载Spring Boot校验相关的自动配置类,上下文启动速度远快于完整@SpringBootTest,更贴近生产运行环境,适合需要少量Spring特性配合的测试场景。
测试代码示例
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import javax.validation.ConstraintViolationException; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) // 仅加载校验相关的自动配置,不启动完整Spring上下文 @ContextConfiguration(classes = {ValidationAutoConfiguration.class, ThingServiceLightTest.TestConfig.class}) public class ThingServiceLightTest { @MockBean private ThingRepository repo; @Autowired private ThingService service; @Configuration static class TestConfig { @Bean public ThingService thingService(ThingRepository repo) { return new ThingService(repo); } } // 原有测试用例完全无需修改,直接复用即可 @Test public void createSuccess() { when(repo.save(any(Thing.class))).thenAnswer(returnsFirstArg()); Thing thing = service.create(2L, new Thing(5L, "foo")); // 原有断言、验证逻辑 } @Test public void createNullArg() { ConstraintViolationException ex = assertThrows(ConstraintViolationException.class, () -> { service.create(null, new Thing(5L, "foo")); }); // 原有异常断言逻辑 } }
方案对比
- 方案1:完全脱离Spring容器,运行速度最快,适合纯单元测试场景
- 方案2:保留轻量Spring上下文,兼容其他Spring注解能力,适合需要少量Spring特性配合的测试场景
内容的提问来源于stack exchange,提问作者Mark Davoren
相关产品推荐
相关产品推荐

