Search code examples
springvalidationtestinghibernate-validator

Test Custom Validator with Autowired spring Service


I have a custom Hibernate Validator for my entities. One of my validators uses an Autowired Spring @Repository. The application works fine and my repository is Autowired successfully on my validator.

The problem is i can't find a way to test my validator, cause i can't inject my repository inside it.

Person.class:

@Entity
@Table(schema = "dbo", name = "Person")
@PersonNameMustBeUnique
public class Person {

    @Id
    @GeneratedValue
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @Column()
    @NotBlank()
    private String name;

    //getters and setters
    //...
}

PersonNameMustBeUnique.class

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { PersonNameMustBeUniqueValidator.class })
@Documented
public @interface PersonNameMustBeUnique{
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends javax.validation.Payload>[] payload() default {};
}

The validator:

public class PersonNameMustBeUniqueValidatorimplements ConstraintValidator<PersonNameMustBeUnique, Person> {

    @Autowired
    private PersonRepository repository;

    @Override
    public void initialize(PersonNameMustBeUnique constraintAnnotation) { }

    @Override
    public boolean isValid(Person entidade, ConstraintValidatorContext context) {
        if ( entidade == null ) {
            return true;
        }

        context.disableDefaultConstraintViolation();

        boolean isValid = nameMustBeUnique(entidade, context);

        return isValid;
    }

    private boolean nameMustBeUnique(Person entidade, ConstraintValidatorContext context) {
        //CALL REPOSITORY TO CHECK IF THE NAME IS UNIQUE 
        //ADD errors if not unique...
    }
}

And the context file has a validator bean:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

Again, it works fine, but i don't know how to test it.

My test file is:

@RunWith(MockitoJUnitRunner.class)
public class PersonTest {

    Person e;
    static Validator validator;

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

    @Test
    public void name__must_not_be_null() {
        e = new Person();
        e.setName(null);
        Set<ConstraintViolation<Person>> violations = validator.validate(e);
        assertViolacao(violations, "name", "Name must not be null");
    }

}

Solution

  • On @BeforeClass:

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

    And in your test you need to replace the beans with your mocked bean:

    myValidator.initialize(null);
    BeanValidatorTestUtils.replaceValidatorInContext(validator, usuarioValidoValidator, e);
    

    The class that do all the magic:

    public class BeanValidatorTestUtils {
    
        @SuppressWarnings({ "rawtypes", "unchecked" })
        public static <A extends Annotation, E> void replaceValidatorInContext(Validator validator,
                                                                                final ConstraintValidator<A, ?> validatorInstance,
                                                                                    E instanceToBeValidated) {
            final Class<A> anotacaoDoValidador = (Class<A>)
                                                    ((ParameterizedType) validatorInstance.getClass().getGenericInterfaces()[0])
                                                        .getActualTypeArguments()[0];
    
            ValidationContextBuilder valCtxBuilder = ReflectionTestUtils.<ValidationContextBuilder>invokeMethod(validator,
                                                                                                    "getValidationContext");
            ValidationContext<E> validationContext = valCtxBuilder.forValidate(instanceToBeValidated);
            ConstraintValidatorManager constraintValidatorManager = validationContext.getConstraintValidatorManager();
    
            final ConcurrentHashMap nonSpyHashMap = new ConcurrentHashMap();
            ConcurrentHashMap spyHashMap = spy(nonSpyHashMap);
            doAnswer(new Answer<Object>() {
                @Override public Object answer(InvocationOnMock invocation) throws Throwable {
                    Object key = invocation.getArguments()[0];
                    Object keyAnnotation = ReflectionTestUtils.getField(key, "annotation");
                    if (anotacaoDoValidador.isInstance(keyAnnotation)) {
                        return validatorInstance;
                    }
                    return nonSpyHashMap.get(key);
                }
            }).when(spyHashMap).get(any());
    
            ReflectionTestUtils.setField(constraintValidatorManager, "constraintValidatorCache", spyHashMap);
        }
    
    }