首页 > 解决方案 > 使用 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;
       }
    }
}

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

标签: javajunit5

解决方案


基本上不必使用反射只需创建一个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返回的方法truefalse

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

@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 方法测试其他内容。我的意思是例如错误消息具有预期的格式和参数或类似的东西。

总的来说,我知道创建许多具有不同注释属性的不同字段很无聊,但我非常喜欢这种方式,因为您可以测试您需要的有关验证器的所有内容。


推荐阅读