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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 19:45:38