如何在Java中测试实现ConstraintValidator的验证器?

26

我有一个名为"AllowedValuesValidator.java"的类:

public class AllowedValuesValidator implements ConstraintValidator<AllowedValues, String> {

    String[] values;
    String defaultValue;

    @Override
    public void initialize(AllowedValues constraintAnnotation) {
        values = constraintAnnotation.allowedValues();
        defaultValue = constraintAnnotation.defaultValue();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!StringUtils.isEmpty(defaultValue) && StringUtils.isEmpty(value)) {
            value = defaultValue;
        }

        if (!StringUtils.isEmpty(value) && !Arrays.asList(values).contains(value)) {
            return false;
        }
        return true;
    }
}

对应的接口类:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AllowedValuesValidator.class)
public @interface AllowedValues {

    String message();

    String fieldName();

    int fieldNumber();

    String[] allowedValues() default {"Y", "N"};

    String defaultValue() default "";
}

我希望能够编写一个单元测试类来测试验证器中的直接逻辑。但是似乎我找到的大多数资源都提供了针对给定模型类测试其所有验证器的测试类示例,例如:

    @BeforeClass
    public static void setup() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void testEmailExistsIncorrect() {

        Set<constraintviolation<usercredentialsdto>> violations = validator
                .validate(credentials, UserCredentialsDto.class);
        Assert.assertEquals(1, violations.size());
    }

我不想建立虚拟模型来测试所有验证器。 有没有一种方法可以创建一个单独的测试类,只直接测试一个单一验证器中的逻辑,而不使用任何其他模型类等?


实际上,我在测试类中创建了一个小的模拟类,并验证该类中的属性似乎足够容易,所以这对我来说很好用。 - goe
根据官方文档(https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidator.html),isValid()方法需要是线程安全的。当使用initialize设置此类的属性时,它是否是线程安全的?每个被验证的字段都会创建此验证器类的新实例吗? - user1884155
6个回答

14

您可以单独测试验证器。问题在于初始化方法,因为它需要一个注解的实例。您基本上有三个选项:

  1. 添加第二个初始化方法,直接传入所需的参数。然后,您可以使用此方法初始化验证器。如果测试位于同一包中,则还可以使此方法仅对该包可见。
  2. 将测试注释放置在测试类的某个位置,并通过反射检索它,以便将其传递给初始化方法。
  3. 使用注释代理。这也是Hibernate Validator在配置约束时或需要进行测试时内部使用的内容。 Hibernate Validator中有两个类可供使用:AnnotationDescriptor和AnnotationFactory。代码应如下所示:

--

private AllowedValues createAnnotation(String[]values, String defaultValue) {
  AnnotationDescriptor<AllowedValues> descriptor = new AnnotationDescriptor<AllowedValues>( AllowedValues.class );
  descriptor.setValue( "values", values );
  descriptor.setValue( "defaultValue", defaultValue );

  return AnnotationFactory.create( descriptor );
}

为了测试目的,您需要依赖于Hibernate Validator内部类,但这应该是可以接受的。当然,您也可以创建自己的代理框架。


10

我使用了下面这个模式:

@RunWith(MockitoJUnitRunner.class)
public class AllowedValuesValidatorTest {

    @Mock
    AllowedValuesValidator allowedValuesValidator;

    @Mock
    ConstraintValidatorContext constraintValidatorContext;

    @Before
    public void setUp() {

        doCallRealMethod().when(allowedValuesValidator).initialize(any());
        when(allowedValuesValidator.isValid(any(), any())).thenCallRealMethod();

        AllowedValuesValidatorTestClass testClass = new AllowedValuesValidatorTestClass();

        allowedValuesValidator.initialize(testClass);

    }

    @Test
    public void testIsValidWithValidValues() {
        assertTrue(allowedValuesValidator.isValid("Value", constraintValidatorContext));
    }

    private class AllowedValuesValidatorTestClass implements AllowedValues {

        @Override
        public String message() {
            return "Test Message";
        }

        @Override
        public Class<?>[] groups() {
            return new Class[]{};
        }

        @Override
        public Class<? extends Payload>[] payload() {
            return new Class[]{};
        }

        @Override
        public Class<? extends Annotation> annotationType() {
            return AllowedValues.class;
        }

    }

}

我们可以对我们正在测试的类进行模拟。由于注释只是一个接口,因此我们可以将一个具体的实现作为参数传递以初始化(您可以使其以任何需要的方式行为以正确初始化测试)。然后,您可以将模拟的ConstraintValidatorContext传递给您的isValid方法。但是,根据该方法执行的操作,您可能需要做一些额外的工作,如果它与上下文进行交互,则可能需要进行更多的模拟。


如果您从验证器中删除模拟,则还可以删除“doCallRealMethod()”。 - lrkwz

5

正如@Rammgarot建议的那样,它可以正常工作,但如果我们更改他的代码,例如验证规则必须是下一个:"没有空格",并且必须以单词"stack"开头
NoWhitesSpace.java

@Documented
@Constraint(validatedBy = NoWhitespacesValidator.class)
@Target({ FIELD })
@Retention(RUNTIME)
public @interface NoWhitesSpace {
  
    String value() default "stack"; 
    String message() default "Not valid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}



NoWhitespacesValidator.java

public class NoWhitespacesValidator implements ConstraintValidator<NoWhitesSpace, String> {


   String prefix;

   public void initialize(NoWhitesSpace constraint) {
     prefix =  constraint.value();
   }

   public boolean isValid(String value, ConstraintValidatorContext context) {

      boolean result = !value.contains(" ") && value.startsWith(prefix);

      return result;

   }
}

测试不会通过,代码会生成NullPointerException。

对我来说,我用mock找到了解决方案。
-> Customer是一个简单的类实体,具有字段、setter和getter。

NoWhitespacesValidatorTest.java

class NoWhitespacesValidatorTest {

    private NoWhitesSpace noWhitesSpace = mock(NoWhitesSpace.class);

    private ConstraintValidatorContext constraintValidatorContext = mock(ConstraintValidatorContext.class);

    @Test
    public void itShouldBeValidWhenItStartsWithPrefixAndWithoutWhiteSpace(){

        when(noWhitesSpace.value()).thenReturn("stack");

        NoWhitespacesValidator noWhitespacesValidator=new NoWhitespacesValidator();
        noWhitespacesValidator.initialize(noWhitesSpace);

        Customer customer = new Customer();
        customer.setCourseCode("stack");

        boolean result = noWhitespacesValidator.isValid(customer.getCourseCode(),constraintValidatorContext);

        assertTrue(result);
    }


}

5

注解:

@Documented
@Constraint(validatedBy = NoWhitespacesValidator.class)
@Target({ FIELD })
@Retention(RUNTIME)
public @interface NoWhitespaces {
    String message() default "Not valid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

验证器:

public class NoWhitespacesValidator implements ConstraintValidator<NoWhitespaces, String> {
    @Override public boolean isValid(String value, ConstraintValidatorContext context) {
        return !value.contains(" ");
    }
}

测试用例:

class NoWhitespacesTest {

    private NoWhitespacesValidator validator = new NoWhitespacesValidator();

    @Nested
    class NoWhitespaceValidFlow {
        @Test
        void isValid_shouldReturnTrue_whenNoWhitespaces() {
            assertTrue(isValid(""));
            assertTrue(isValid("foo.bar"));
        }
    }

    @Nested
    class NoWhitespacesInvalidFlow {
        @Test
        void isValid_shouldReturnFalse_whenAtLeastOneWhitespace() {
            assertFalse(isValid(" "));
            assertFalse(isValid("foo bar"));
            assertFalse(isValid("  foo"));
            assertFalse(isValid("foo  "));
        }
    }

    private boolean isValid(String value) {
        return validator.isValid(value, null);
    }
}

1
我想知道这个怎么能编译,因为你的 isValid() 方法需要一个 ConstraintValidationContext。 - lrkwz

2
Hardy的答案很好,但是如果您使用hibernate-validator-6.5.1.Final版本,@Hardy所使用的构造函数AnnotationDescriptor将有另一个参数,并且它是私有的。 您可以在此处检查AnnotationDescriptor构造函数 here。 因此,到目前为止,我只是结合了@Rammgarot和@JCollerton的答案。 这是测试验证器的另一种方法。
public class AllowedValuesValidatorTest {

    private AllowedValuesValidator validator;
    String[] allowedValues = {"Y", "N"};
    String defaultValue = "";

    @BeforeEach
    void setUp() {
        validator = new AllowedValuesValidator();
        validator.initialize(createAnnotation(allowedValues, defaultValue));
    }

    @Test
    void isValid() {
        // your test here
    }

    private boolean isValid(String value) {
        return validator.isValid(value, null);
    }

    private AllowedValues createAnnotation(String[] allowedValues, String defaultValue) {
        return new AllowedValues() {
            @Override
            public Class<? extends Annotation> annotationType() {
                return null;
            }

            @Override
            public String message() {
                return "Please provide a valid id";
            }

            @Override
            public Class<?>[] groups() {
                return new Class[0];
            }

            @Override
            public Class<? extends Payload>[] payload() {
                return new Class[0];
            }

            @Override
            public String[] allowedValues() {
                return allowedValues;
            }

             @Override
             public String defaultValue() {
                return defaultValue;
             }
        };
    }
}

0
这是一个老问题,但我碰巧在寻找如何做同样的事情的技巧(没有找到好的方法)。我整理了以下测试,感觉这是一个适当/合理的方法。
我觉得模拟东西并不是很方便。相反,约束应该通过使用框架本身来进行测试。 这是我如何使约束验证与Spring Boot项目一起工作的方法。
我不提供约束本身的代码(与周围的示例没有区别),只提供一种清晰的方法来实际测试它。
  1. src/test 下创建一个简单的服务。(不要为了测试目的而在 src/main 中创建一个虚拟服务)

     import org.springframework.stereotype.Service;
     import org.springframework.validation.annotation.Validated;
     import annotation.NewUserConstraint;
     import entity.UserInfo;
    
     @Service
     @Validated // 重要:没有这个约束将无法生效
     public class ConstraintTestingService {
    
         /**
          * 根据 {@link NewUserConstraint} 中指定的约束规则对参数进行验证。
          * @param user 约束验证的目标对象。
          * @return 当验证通过时返回<code>True</code>。
          */
         public boolean testConstraint(@NewUserConstraint UserInfo user) {
             return true;
         }
      }
    
  2. 创建一个 SpringBoot 测试。

     import java.util.Set;
     import javax.validation.ConstraintViolation;
     import javax.validation.ConstraintViolationException;
    
     import org.junit.jupiter.api.Assertions;
     import org.junit.jupiter.api.Test;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.boot.test.context.SpringBootTest;
     import org.springframework.test.context.ActiveProfiles;
    
     import ConstraintTestingService;
     import entity.UserInfo;
    
     @SpringBootTest
     @ActiveProfiles("test")
     public class ConstraintTest extends Assertions {
     @Autowired
     private ConstraintTestingService constraintTest;
    
     @Test
     public void testNewUserConstraint() {
         var user = new UserInfo("12345");
         var exception = assertThrows(ConstraintViolationException.class, () -> this.constraintTest.testConstraint(user));
         var violations = exception.getConstraintViolations();
         assertNotNull(violations);
         assertEquals(2, violations.size());
         // 进行其他必要的操作来检查违规的验证消息
     }
    

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接