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:
- 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
- Place the test annotation somewhere into your test class and retrieve it via reflection in order to pass it to the initialize method.
- 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;
}
};
}
}