Test Custom Validator with Autowired spring Service

点点圈 提交于 2019-12-01 01:53:25

问题


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

}

回答1:


Recently I had the same problem with my custom validator. I needed to validate a model being passed to a controller's method (method level validation). The validator invoked but the dependencies (@Autowired) could not be injected. It took me some days searching and debugging the whole process. Finally, I could make it work. I hope my experience save some time for others with the same problem. Here is my solution:

Having a jsr-303 custom validator like this:

@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 should configure spring test 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, here I am using testng, but you can use JUnit 4. The whole configuration would be the same except that you would run the test with @RunWith(SpringJUnit4ClassRunner.class) and do not extend the AbstractTestNGSpringContextTests.

Now, @ValidSample can be used in places mentioned in @Target() of the custom annotation. Attention: If you are going to use the @ValidSample annotation on method level (like validating method arguments), then you should put class level annotation @Validated on the class where its method is using your annotation, for example on a controller or on a service class.




回答2:


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

}



回答3:


I was facing very similar problem: How to write pure unit test of custom validator wich has autowired configuration bean?

I could manage to solve it by following code (inspired by this answer of user abhishekrvce).

This is pure unit test of custom validator with @Autowired configuration bean, which reads the data from configuration file (not showed in 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);
  }

}



回答4:


We also faced the similar problem where @Autowiring was failing (not initialised) in ConstrainValidator Class. Our ConstraintValidator Implemented class was using a value which supposed to be read from the application.yml file. Below solution helped us as this is using a pure spring scope. Hope this helps, with 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;

}



回答5:


Spring Boot 2 allows to inject Bean in custom Validator without any fuss.The Spring framework automatically detects all classes which implement the ConstraintValidator interface, instantiate them, and wire all dependencies.

I had Similar problem , this is how i have 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);

    }

} 

Usage

@Component
@Validated
public class PersonService {

    @Autowired
    PersionList personRepository;

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



回答6:


U can add the following bean to your Spring Context in the test:

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

  @Autowired
  private Validator validator;

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

  ...
}


来源:https://stackoverflow.com/questions/25610061/test-custom-validator-with-autowired-spring-service

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!