使用 JUnit 5 测试自定义约束注释

Test custom constraint annotations with JUnit 5

实现自定义约束注释,例如 @MySize 需要我使用单元测试来测试它是否正常运行:

public class MySizeTest {
   @Test
   public void noMinMax() {
       Dummy dummy = new Dummy();
       // some asserts or so
       dummy.setMyField("");
       dummy.setMyField(null);
       dummy.setMyField("My text");
   }

   @Test
   public void onlyMin() {
       // change @MySize to have min: @MySize(min = 1)
       ... how?
       ... then test with some setMyField:
       Dummy dummy = new Dummy();
       // some asserts or so
       dummy.setMyField("");
       dummy.setMyField(null);
       dummy.setMyField("My text");
   }

   @Test
   public void onlyMax() {
       // change @MySize to have max: @MySize(max = 50)
       ...
   }

   @Test
   public void bothMinMax() {
       // change @MySize to have min and max: @MySize(min = 1, max = 50)
       ...
   }

   private class Dummy {
       @MySize()
       String myField;

       public String getMyField() {
           return myField;
       }

       public void setMyField(String myField) {
           this.myField = myField;
       }
    }
}

我认为这必须通过反思来完成,但我不知道如何做。

基本上不必使用反射,只需创建一个 Validator 实例并将其用于验证。

例如:

当注解为:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyValidator.class)
public @interface MyAnnotation {
    String message() default "Invalid value (it must be foo)";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

相关的验证器是:

public class MyValidator implements ConstraintValidator<MyAnnotation, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s) return true;
        return "foo".equalsIgnoreCase(s);
    }
}

那么测试应该是这样的:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyValidatorTest {

    private Validator validator;

    @BeforeAll
    void init() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    private static class TestObject {

        @MyAnnotation
        private String testField;

        TestObject() {
            this(null);
        }

        TestObject(String value) {
            testField = value;
        }

        public String getTestField() {
            return testField;
        }

        public void setTestField(String testField) {
            this.testField = testField;
        }
    }

    @Test
    void shouldValidForNullValue() {
        var obj = new TestObject();
        var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
        Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
    }

    @Test
    void shouldValidForFooValue() {
        var obj = new TestObject("foo");
        var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
        Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
    }

    @Test
    void shouldInvalidForBarValue() {
        var obj = new TestObject("bar");
        var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
        Assertions.assertEquals(1, violations.size());
    }
}

更新(2020.05.21.)- 使用属性和 AnnotationFactory

根据评论,我更新了我的答案。 如果您只想测试验证逻辑,那么只需创建一个 Annotation 实例并调用 isValid 方法,即 returns truefalse

Hibernate Validator 提供了AnnotationFactory.create(...) 方法来制作注解实例。 之后,您可以创建自定义验证器的实例并在测试用例中调用 initializeisValid 方法。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyHasAttributesValidator.class)
public @interface MyAnnotationHasAttributes {
    String message() default "Invalid value (it must be foo)";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    int attributeOne() default 10;
    int attributeTwo() default 20;

}

相关验证者:

public class MyHasAttributesValidator implements ConstraintValidator<MyAnnotationHasAttributes, String> {
    private MyAnnotationHasAttributes ann;

    @Override
    public void initialize(MyAnnotationHasAttributes constraintAnnotation) {
        ann = constraintAnnotation;
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s) return true;
        return s.length() >= ann.attributeOne() && s.length() < ann.attributeTwo();
    }
}

和修改后的测试(断言失败):

public class HasAttributeValidatorTest {

    private MyAnnotationHasAttributes createAnnotation(Integer one, Integer two) {
        final Map<String, Object> attrs = new HashMap<>();
        if (null != one) {
            attrs.put("attributeOne", one);
        }
        if (null != two) {
            attrs.put("attributeOne", two);
        }
        var desc = new AnnotationDescriptor.Builder<>(MyAnnotationHasAttributes.class, attrs).build();
        return AnnotationFactory.create(desc);
    }

    @ParameterizedTest
    @MethodSource("provideValues")
    void testValidator(Integer one, Integer two, String input, boolean expected) {
        MyAnnotationHasAttributes ann = createAnnotation(one, two);
        MyHasAttributesValidator validator = new MyHasAttributesValidator();
        validator.initialize(ann);
        var result = validator.isValid(input, null);
        Assertions.assertEquals(expected, result, String.format("Validation must be %s but found: %s with params: %d, %d, %s", expected, result, one, two, input));
    }

    private static Stream<Arguments> provideValues() {
        return Stream.of(
                Arguments.of(null, null, null, true),
                Arguments.of(null, 20, "foo", true),
                Arguments.of(null, null, RandomStringUtils.randomAlphabetic(30), false)
        );
    }
}

此解决方案的局限性

供应商锁定

在这种情况下,您的测试使用 Hibernate Validator,它是 Bean Validation 标准的特定实现。老实说,我不认为这是一个大问题,因为 Hibernate Validator 是参考实现和最流行的 bean 验证库。但从技术上讲,它是供应商锁。

跨字段验证不可用

此解决方案仅适用于单场情况。如果您有例如跨域验证器(例如密码和确认密码匹配),则此示例不适合。

类型独立验证需要更多工作

如前所述 @Size 注释属于基于类型(基元、集合、字符串等)的几种不同验证器实现。 使用此解决方案,您总是必须手动选择特定的验证器并对其进行测试。

只能测试isValid方法

在这种情况下,您将无法仅测试 isValid 方法。我的意思是例如错误消息具有预期的格式和参数或类似的内容。

在某种程度上,我知道创建许多具有不同注释属性的不同字段很无聊,但我强烈更喜欢这种方式,因为您可以测试验证器所需的一切。