简体   繁体   中英

How to test a Validator which implements ConstraintValidator in java?

I have an "AllowedValuesValidator.java" class:

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;
    }
}

And the corresponding interface class:

@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 "";
}

I want to be able to write a unit test class to test the direct logic in that validator. But it seems that most places I googled give examples of test classes where we basically test all validators for a given Model class, for example:

    @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());
    }

I don't want to build mock models to test all validators. Is there a way to create a separate test class for just testing the logic in one single validator directly without using any other model classes etc?

You can test the validator standalone. The rub is of course the initialize method, since it needs an instance of the annotation. You basically have three options:

  1. Add a second initialize method which takes the required parameters directly. You can then use this method to initialize the validator. You can also make this method just package visible, provided your test resides in the same package
  2. Place the test annotation somewhere into your test class and retrieve it via reflection in order to pass it to the initialize method.
  3. Use annotation proxies. This is also what Hibernate Validator itself uses internally for in case constraints are configured via XML or needed for tests. There are two classes in Hibernate Validator which you could use AnnotationDescriptor and AnnotationFactory . The code would somewhat like this:

--

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 );
}

You would need to depend on Hibernate Validator internal classes, but for testing purposes this should be fine. Of course you could also just create your own proxy framework.

I used the below pattern:

@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;
        }

    }

}

We can mock the class we are testing. As an annotation is just an interface we can pass in a concrete implementation as the parameter to initialise (which you can make behave any way you need in order to initialise your test correctly). You can then pass in a mock ConstraintValidatorContext to your isValid method. However, you may need to do some extra work depending on what that method does, if it interacts with the context you may need to do some further mocking.

Annotation:

@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 {};
}

Validator:

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

TestCase:

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);
    }
}

@Hardy's answer is pretty good but if you use hibernate-validator-6.5.1.Final the constructor that @Hardy's used AnnotationDescriptor has another parameter and also it is private. You can check AnnotationDescriptor constructors here . So what i did so far, just combine @Rammgarot and @JCollerton answers. Here is the another way to test validator.

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;
             }
        };
    }
}

As @Rammgarot suggest , it works fine but if we change his code , for example the validations rules must be next : "no white spaces" and must begin with the word "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;

   }
}

The test will not pass and the code will generate NullPointerException.

For me, i find the solution with mock.
-> Customer is a simple class entity with fields , setters and getters.

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);
    }


}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM