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");
}
}
source to share
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);
}
}
source to share
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.
source to share
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);
}
}
source to share
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;
}
source to share
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);
}
}
source to share