SpringBoot测试中如何Mock jakarta.validation.ConstraintValidator及其依赖?
问题分析与解决思路
在SpringBoot 3.0.5 + Java 17的@WebMvcTest场景下,自定义校验器的Mock失效核心原因有两点:
ConstraintValidator实例由JSR-380的ValidatorFactory创建,默认不直接受Spring容器管理,导致直接Mock的ClockProvider或校验器无法被校验上下文获取;ConstraintValidatorContext自带的ClockProvider并非Spring容器内的Bean,直接注册ClockProviderBean不会自动关联到校验上下文。
以下是两种可行的解决方案:
方案一:重构校验器,依赖Spring注入的时钟与配置
将校验器改为Spring管理的Bean,直接注入Clock和配置类,不再从ConstraintValidatorContext获取时钟,让测试Mock更直接。
1. 修改校验器代码
@Component public class ValidNotificationDateValidator implements ConstraintValidator<ValidNotificationDate, DateRange> { private final Clock clock; private final NotificationPropertiesConfig notificationPropertiesConfig; // 构造注入Spring管理的依赖 public ValidNotificationDateValidator(Clock clock, NotificationPropertiesConfig notificationPropertiesConfig) { this.clock = clock; this.notificationPropertiesConfig = notificationPropertiesConfig; } @Override public boolean isValid(DateRange value, ConstraintValidatorContext context) { // 使用注入的时钟替代context.getClockProvider() final Instant lowestPossibleLookup = Instant.now(clock).minus(notificationPropertiesConfig.getExpiryMax()); // 后续校验逻辑... } }
2. 修改测试代码
直接注册测试时钟Bean,并Mock配置类:
@WebMvcTest(controllers = VeryNiceController.class) @Import({WebSecurityConfig.class, RequestIdService.class, ObjectMapperConfig.class}) @TestPropertySource(locations = "classpath:mock-user.properties") public class VeryNiceControllerTest { static final Clock TEST_CLOCK = Clock.fixed(Instant.ofEpochSecond(1627983134), ZoneId.of("UTC")); @Autowired MockMvc mockMvc; @MockBean NotificationPropertiesConfig propertiesConfig; // 注册测试时钟Bean,覆盖默认时钟 @Bean public Clock testClock() { return TEST_CLOCK; } @Test @WithMockUser void testVeryNiceEndpoint() throws Exception { doReturn(Period.ZERO).when(propertiesConfig).getExpiryMax(); this.mockMvc.perform(get("/very/nice")) .andDo(print()) .andExpect(status().isOk()); } }
方案二:配置ValidatorFactory,替换校验上下文的ClockProvider
如果不想修改校验器原有逻辑,可通过配置Spring的LocalValidatorFactoryBean,指定自定义的ClockProvider实现,让校验上下文使用测试时钟。
修改测试代码
@WebMvcTest(controllers = VeryNiceController.class) @Import({WebSecurityConfig.class, RequestIdService.class, ObjectMapperConfig.class}) @TestPropertySource(locations = "classpath:mock-user.properties") public class VeryNiceControllerTest { static final Clock TEST_CLOCK = Clock.fixed(Instant.ofEpochSecond(1627983134), ZoneId.of("UTC")); @Autowired MockMvc mockMvc; @SpyBean NotificationPropertiesConfig propertiesConfig; @TestConfiguration static class VeryNiceConfiguration { @Bean public LocalValidatorFactoryBean validatorFactoryBean() { LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean(); // 指定自定义ClockProvider的全类名 factory.getValidationPropertyMap().put("javax.validation.clock.provider", TestClockProvider.class.getName()); return factory; } // 自定义测试用ClockProvider public static class TestClockProvider implements ClockProvider { @Override public Clock getClock() { return TEST_CLOCK; } } } @Test @WithMockUser void testVeryNiceEndpoint() throws Exception { doReturn(Period.ZERO).when(propertiesConfig).getExpiryMax(); this.mockMvc.perform(get("/very/nice")) .andDo(print()) .andExpect(status().isOk()); } }
额外优化建议
如果NotificationPropertiesConfig是@ConfigurationProperties注解的配置类,可直接在mock-user.properties中添加配置值替代Mock:
# 对应Period.ZERO的配置格式 notification.expiry-max=P0D
这样无需使用@SpyBean或@MockBean,测试代码更简洁。
内容的提问来源于stack exchange,提问作者Antônio Quadrado
相关产品推荐
相关产品推荐

