Quarkus中含@Inject的自定义ConstraintValidator单元测试方案
解决方案:Quarkus 带依赖注入的自定义 ConstraintValidator 单元测试
你的问题核心在于:纯 JUnit+Mockito 测试中,JSR-380 的 Validator 工厂不会触发 Quarkus 的 CDI 容器,导致自定义 Validator 里的 @Inject 和 @ConfigProperty 依赖为空;同时默认校验规则触发它,大概率是 Validator 的泛型或注解范围没限制好。以下是两种可行方案:
方案1:用 Quarkus 单元测试框架(推荐)
Quarkus 的 @QuarkusUnitTest 会启动轻量 CDI 容器,自动处理依赖注入,还能通过注解轻松 mock 依赖、覆盖配置,完美模拟运行时环境。
示例代码
自定义 Constraint 与 Validator
import jakarta.validation.Constraint; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.Payload; import jakarta.inject.Inject; import io.smallrye.config.ConfigProperty; import java.lang.annotation.*; @Target({FIELD, PARAMETER}) @Retention(RUNTIME) @Constraint(validatedBy = BookExistsValidator.class) public @interface BookExists { String message() default "指定书籍不存在"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class BookExistsValidator implements ConstraintValidator<BookExists, String> { @Inject BookService bookService; @ConfigProperty(name = "app.book.validation.enabled") boolean validationEnabled; @Override public boolean isValid(String bookId, ConstraintValidatorContext context) { if (!validationEnabled) return true; if (bookId == null) return false; return bookService.isBookExists(bookId); } }
测试类
import jakarta.inject.Inject; import jakarta.validation.Validator; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusUnitTest; import io.quarkus.test.junit.mockito.InjectMock; import io.quarkus.test.junit.TestPropertyOverride; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @QuarkusUnitTest @TestPropertyOverride(properties = {"app.book.validation.enabled=true"}) public class BookValidationTest { @Inject Validator validator; @InjectMock BookService bookService; // 同时测试默认校验规则与自定义校验逻辑 @Test void testValidationScenarios() { // 定义测试用的实体类 class TestBookRequest { @NotNull(message = "书籍ID不能为空") @BookExists String bookId; TestBookRequest(String bookId) { this.bookId = bookId; } } // Mock 业务服务的返回结果 when(bookService.isBookExists("valid-123")).thenReturn(true); when(bookService.isBookExists("invalid-456")).thenReturn(false); // 场景1:书籍ID为null,触发@NotNull var violations = validator.validate(new TestBookRequest(null)); assertEquals(1, violations.size()); assertEquals("书籍ID不能为空", violations.iterator().next().getMessage()); // 场景2:书籍ID存在,但无效,触发@BookExists violations = validator.validate(new TestBookRequest("invalid-456")); assertEquals(1, violations.size()); assertEquals("指定书籍不存在", violations.iterator().next().getMessage()); // 场景3:校验通过 violations = validator.validate(new TestBookRequest("valid-123")); assertTrue(violations.isEmpty()); } }
方案2:纯 JUnit+Mockito 手动注入依赖
如果不想启动 Quarkus 容器,可以手动实例化 Validator 并通过反射注入依赖,适合快速单测自定义校验逻辑。
示例代码
import jakarta.validation.Validation; import jakarta.validation.Validator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.lang.reflect.Field; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; public class BookExistsValidatorTest { @Mock BookService bookService; private BookExistsValidator customValidator; private Validator jsrValidator; @BeforeEach void setUp() throws NoSuchFieldException, IllegalAccessException { MockitoAnnotations.openMocks(this); customValidator = new BookExistsValidator(); // 手动注入依赖:通过反射设置私有字段 Field bookServiceField = BookExistsValidator.class.getDeclaredField("bookService"); bookServiceField.setAccessible(true); bookServiceField.set(customValidator, bookService); Field validationEnabledField = BookExistsValidator.class.getDeclaredField("validationEnabled"); validationEnabledField.setAccessible(true); validationEnabledField.set(customValidator, true); // 将自定义Validator注册到JSR-380 Validator工厂 jsrValidator = Validation.byDefaultProvider() .configure() .addValidators(customValidator) .buildValidatorFactory() .getValidator(); } @Test void testCustomValidatorLogic() { class TestBook { @BookExists String bookId; TestBook(String bookId) { this.bookId = bookId; } } when(bookService.isBookExists("valid-id")).thenReturn(true); // 测试有效ID var violations = jsrValidator.validate(new TestBook("valid-id")); assertTrue(violations.isEmpty()); // 测试无效ID violations = jsrValidator.validate(new TestBook("invalid-id")); assertEquals(1, violations.size()); assertEquals("指定书籍不存在", violations.iterator().next().getMessage()); } // 验证默认校验规则不会误触发自定义Validator @Test void testDefaultValidationIsolated() { class TestBook { @NotNull String bookId; } // 仅@NotNull校验,不会触发BookExistsValidator var violations = jsrValidator.validate(new TestBook(null)); assertEquals(1, violations.size()); assertEquals("must not be null", violations.iterator().next().getMessage()); } }
关键注意事项
- 检查自定义 Validator 的泛型:确保是
ConstraintValidator<BookExists, String>而非ConstraintValidator<BookExists, Object>,避免被无关类型的校验触发。 - 优先选择方案1:Quarkus 测试框架能完整模拟运行时的 CDI 环境,减少手动注入的繁琐和潜在问题。
内容的提问来源于stack exchange,提问作者Mr_mufy
相关产品推荐
相关产品推荐

