You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot测试中如何Mock jakarta.validation.ConstraintValidator及其依赖?

问题分析与解决思路

在SpringBoot 3.0.5 + Java 17的@WebMvcTest场景下,自定义校验器的Mock失效核心原因有两点:

  1. ConstraintValidator实例由JSR-380的ValidatorFactory创建,默认不直接受Spring容器管理,导致直接Mock的ClockProvider或校验器无法被校验上下文获取;
  2. ConstraintValidatorContext自带的ClockProvider并非Spring容器内的Bean,直接注册ClockProvider Bean不会自动关联到校验上下文。

以下是两种可行的解决方案:


方案一:重构校验器,依赖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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 09:10:09