Validating custom validator with Autowired spring function

I have a custom Hibernate Validator for my entities. One of my validators is using Autowired Spring @Repository. The app is working fine and my repository completed successfully on my validator.

The problem is I can't find a way to validate my validator because I can't inject my repository into 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 {};
}

      

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 there is a bean validator in the context file:

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

      

Again, it works great, but I don't know how to test it.

My test file:

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

}

      

+6


source to share


6 answers


In the @BeforeClass field:

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

      

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



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

      

The class that does 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);
    }

}

      

+3


source


I recently had the same problem with my custom validator. I needed to check the model passed to the controller method (method level check). A validator being called, but dependencies (@Autowired) could not be injected. It took me days to find and debug the whole process. Finally, I could make it work. Hope my experience saves time for others with the same problem. Here is my solution:

Having a custom jsr-303 validator as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD,
      ElementType.PARAMETER,
      ElementType.TYPE,
      ElementType.METHOD,
      ElementType.LOCAL_VARIABLE,
      ElementType.CONSTRUCTOR,
      ElementType.TYPE_PARAMETER,
      ElementType.TYPE_USE })
@Constraint(validatedBy = SampleValidator.class)
public @interface ValidSample {
    String message() default "Default sample validation error";

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

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

}

public class SampleValidator implements ConstraintValidator<ValidSample, SampleModel> {

    @Autowired
    private SampleService service;


    public void initialize(ValidSample constraintAnnotation) {
    //init
    }

    public boolean isValid(SampleModel sample, ConstraintValidatorContext context) {
    service.doSomething();
    return true;
    }


}

      

You need to configure your spring tag like this:



    @ComponentScan(basePackages = { "your base packages" })
    @Configurable
    @EnableWebMvc
    class SpringTestConfig {
        @Autowired
        private WebApplicationContext wac;

    @Bean
    public Validator validator() {
    SpringConstraintValidatorFactory scvf = new SpringConstraintValidatorFactory(wac.getAutowireCapableBeanFactory());
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setConstraintValidatorFactory(scvf);
    validator.setApplicationContext(wac);
    validator.afterPropertiesSet();
    return validator;
    }

    @Bean
    public MethodValidationPostProcessor mvpp() {
    MethodValidationPostProcessor mvpp = new MethodValidationPostProcessor();
    mvpp.setValidatorFactory((ValidatorFactory) validator());
    return mvpp;
    }

    @Bean
    SampleService sampleService() {
    return Mockito.mock(SampleService.class);
    }

}

@WebAppConfiguration
@ContextConfiguration(classes = { SpringTestConfig.class, AnotherConfig.class })
public class ASampleSpringTest extends AbstractTestNGSpringContextTests {

    @Autowired
    private WebApplicationContext wac;



    private MockMvc mockMvc;


    @BeforeClass
    public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);

    mockMvc = MockMvcBuilders.webAppContextSetup(wac)
                 .build();
    }



    @Test
    public void testSomeMethodInvokingCustomValidation(){
         // test implementation
         // for example:
         mockMvc.perform(post("/url/mapped/to/controller")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json))
                .andExpect(status().isOk());

    }

}

      

Note that I'm using testng here, but you can use JUnit 4. All configuration will be the same except that you run the test with @RunWith (SpringJUnit4ClassRunner.class) and don't extend the AbstractTestNGSpringContextTests tags.

The @ValidSample can now be used in the locations specified in the @Target () of the custom annotation. Attention . If you are going to use @ValidSample annotation at the method level (for example, validation method arguments), then you should put the class level annotation @Validated in the class where its method uses your annotation, for example on a controller or in a service class.

+4


source


I ran into a very similar problem: How do I write a clean unit test for a custom validator that has an auto-wired config component?

I managed to solve this problem with the following code (inspired by this answer from user abhishekrvce ).

This is a pure unit test of a custom validator with the @Autowired config component that reads data from a config file (not shown in the code).

@Import({MyValidator.class})
@ContextConfiguration(classes = MyConfiguration.class, initializers = ConfigFileApplicationContextInitializer.class)
class MyValidatorTest {

  private LocalValidatorFactoryBean validator;

  @Autowired
  private ConfigurableApplicationContext applicationContext;

  @BeforeEach
  void initialize() {
    SpringConstraintValidatorFactory springConstraintValidatorFactory
        = new SpringConstraintValidatorFactory(
        applicationContext.getAutowireCapableBeanFactory());
    validator = new LocalValidatorFactoryBean();
    validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
    validator.setApplicationContext(applicationContext);
    validator.afterPropertiesSet();
  }

  @Test
  void isValid()
  {
    Set<ConstraintViolation<MyObject>> constraintViolations = validator
        .validate(myObjectInstance);
    assertThat(constraintViolations).hasSize(1);
  }

}

      

+2


source


We also ran into a similar issue where @Autowiring was not working (not initialized) in the ConstrainValidator class. Our implemented ConstraintValidator class used a value to be read from a file application.yml

. The solution below helped us as it is using a clean spring frame. Hope this helps, with a proper SpringJunit4ClassRunner.

import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import org.springframework.web.context.WebApplicationContext;

@WebAppConfiguration
@ContextConfiguration(classes = {ApplicationConfig.class})
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
        "spring.someConfigValue.InApplicationYaml=Value1",
})
public class MyTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    LocalValidatorFactoryBean validator;

    @Before
    public void setup() {

        SpringConstraintValidatorFactory springConstraintValidatorFactory
                    = new SpringConstraintValidatorFactory(webApplicationContext.getAutowireCapableBeanFactory());
            validator = new LocalValidatorFactoryBean();
            validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
            validator.setApplicationContext(webApplicationContext);
            validator.afterPropertiesSet();
    }

    @Test
        public void should_have_no_violations_for_all_valid_fields() {

        Set<ConstraintViolation<PojoClassWhichHaveConstraintValidationAnnotation>> violations = validator.validate(pojoClassObjectWhichHaveConstraintValidationAnnotation);

        assertTrue(violations.isEmpty());
    }

}


@Configuration
public class ApplicationConfig {

    @Value("${spring.someConfigValue.InApplicationYaml=Value1}")
    public String configValueToBeReadFromApplicationYamlFile;

}

      

+1


source


Spring Boot 2 allows you to inject a Bean into a custom Validator without any hassle. The Spring framework automatically discovers all classes that implement the interface ConstraintValidator

, instantiates them, and binds all dependencies.

I had a similar problem, this is how I implemented.

Step 1 Interface

@Documented
@Constraint(validatedBy = UniqueFieldValidator.class)
@Target({ ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueField {

    String message() default "Duplicate Name";

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

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

      

Step 2 Validator

public class UniqueFieldValidator implements ConstraintValidator<UniqueField, Person> {
    @Autowired
    PersionList personRepository;

    private static final Logger log = LoggerFactory.getLogger(PersonRepository.class);

    @Override
    public boolean isValid(Person object, ConstraintValidatorContext context) {

        log.info("Validating Person for Duplicate {}",object);
        return personRepository.isPresent(object);

    }

} 

      

using

@Component
@Validated
public class PersonService {

    @Autowired
    PersionList personRepository;

    public void addPerson(@UniqueField Person person) {
        personRepository.add(person);
    }
}

      

0


source


You can add the following bean to your Spring context in your test:

@RunWith(SpringRunner.class)
@Import(LocalValidatorFactoryBean.class)
public class PersonTest {

  @Autowired
  private Validator validator;

  {
    validator.validate(new Person());
  }

  ...
}

      

0


source







All Articles