I'm using Spring Boot 2.4. I have the following controller with a method that accepts a MultipartFile object.
#RestController
public class MyController extends AbstractController
...
#Override
public ResponseEntity<ResponseData> add(
...
#Parameter(description = "file detail") #Validated #RequestPart("myFile")
MultipartFile myFile,
...
) {
I would like to validate that this MultipartFile contains the data that I want (e.g. is of a particular mime type). So I have written the below validator ...
#Documented
#Constraint(validatedBy = MultipartFileValidator.class)
#Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface MultipartFileConstraint {
String message() default "Incorrect file type.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and its implementation class ...
public class MultipartFileValidator
implements ConstraintValidator<MultipartFileConstraint, MultipartFile> {
#Override
public void initialize(final MultipartFileConstraint constraintAnnotation) {
log.info("\n\n\n\nconstructor called\n\n\n\n");
}
#Override
public boolean isValid(
MultipartFile file, ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
...
}
}
However, when I invoke my endpoint, I don't see that my validator is called (for one, the log statement is never printed nor breakpoints hit). What else do I need to do to register my validator for this MultipartFile param?
As per the Spring Documentation:
Can also be used with method level validation, indicating that a
specific class is supposed to be validated at the method level (acting
as a pointcut for the corresponding validation interceptor), but also
optionally specifying the validation groups for method-level
validation in the annotated class. Applying this annotation at the
method level allows for overriding the validation groups for a
specific method but does not serve as a pointcut; a class-level
annotation is nevertheless necessary to trigger method validation for
a specific bean to begin with. Can also be used as a meta-annotation
on a custom stereotype annotation or a custom group-specific validated
annotation.
So, here we have to keep in mind what are the placement of #Validated and validator annotation.
Code:
Controller class : #Validated added at class level and #ValidFile (Custom validator annotation) in the method
#RestController
#Validated
#Slf4j
public class MyController {
#RequestMapping("/add")
public ResponseEntity<ResponseData> add(#ValidFile #RequestParam("file") MultipartFile file) {
log.info("File Validated");
return ResponseEntity.status(HttpStatus.OK).body(new ResponseData("Valid file received"));
}
}
Validator Annotation
#Documented
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {FileValidator.class})
public #interface ValidFile {
Class<? extends Payload> [] payload() default{};
Class<?>[] groups() default {};
String message() default "Only pdf,xml,jpeg,jpg files are allowed";
}
Validator class
#Slf4j
public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
#Override
public void initialize(ValidFile validFile) {
log.info("File validator initialized!!");
}
#Override
public boolean isValid(MultipartFile multipartFile,
ConstraintValidatorContext constraintValidatorContext) {
log.info("Validating file");
String contentType = multipartFile.getContentType();
assert contentType != null;
return isSupportedContentType(contentType);
}
private boolean isSupportedContentType(String contentType) {
return contentType.equals("application/pdf")
|| contentType.equals("text/xml")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg");
}
}
Output :
Success:
{
"message": "Valid file received"
}
Exception handler
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return new ResponseEntity<>("Validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
}
Failure:
Validation error: Only pdf,xml,jpeg,jpg files are allowed
Below is a small example. I hope it will help.
#Component
public class MultipartFileValidator implements Validator {
#Override
public boolean supports(Class < ? > clazz) {
return MultipartFile.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
MultipartFile multipartFile = (MultipartFile) target;
if (multipartFile.isEmpty()) {
// Add an error message to the errors list
errors.rejectValue("file", "required.file");
}
}
}
My purpose is to use a specific ConstraintValidator in 2 scenarios below
use annotations on POJO to validate the whole object (the popular way)
validate a single value with specific validator's isValid function (for some configurable dynamic validation request)
The validator must support services injection, so I must get it from spring but not create a new validator instance manually.
followed my test codes:
annotation
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = { IdNumberValidator.class })
public #interface IdNumber {
String message() default "id number is not available";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
validator
public class IdNumberValidator implements ConstraintValidator<IdNumber, String>, BeanNameAware {
#Autowired
private IUserService usrService;
private String bn;
#Override
public boolean isValid(String value, ConstraintValidatorContext context){
System.out.println("my name is:" + bn);
return usrService.getUserByAccount(value).isPresent();
}
#Override
public void setBeanName(String name) {
this.bn = name;
}
}
pojo
#Getter
#Setter
public class TestPOJO {
private long id;
#IdNumber
private String idn;
}
service
#Service
public class TestValidatorService {
#Autowired
private Validator validator;
#Autowired
private ApplicationContext context;
public void validatePojo(TestPOJO pojo){
BeanPropertyBindingResult e = new BeanPropertyBindingResult(pojo, "TestPOJO");
validator.validate(pojo, e);
if(e.hasErrors()){
for(ObjectError oe : e.getAllErrors()){
System.out.println(oe.toString());
}
}
}
public void validatePojoByDynamicValidator(TestPOJO pojo){
IdNumberValidator validator = context.getBean("com.test.IdNumberValidator", IdNumberValidator.class); // got the name via BeanNameAware but seems not working
System.out.println(validator.isValid(pojo.getIdn(), null));
}
}
In the test case for service, function "validatePojo" passed but "validatePojoByDynamicValidator" did not.
Any solution for this problem? Thanks!
The following code yields null on the teamResource auto-wired object inside isValid() function:
#Component //I am not sure it is required he
public class TeamIdValidator implements ConstraintValidator<TeamIdConstraint, Integer> {
#Autowired private TeamResource teamResource;
public TeamIdValidator() {
}
#Override
public void initialize(TeamIdConstraint teamIdConstraint) {
// this.teamIdConstraint = teamIdConstraint;
}
#Override
public boolean isValid(Integer teamId, ConstraintValidatorContext cxt) {
int numOfAvailableTeams = teamResource.retrieveAllTeams().size(); //teamResource is null :(
return teamId < 0 || teamId >= numOfAvailableTeams;
}
}
TeamResource class:
#RestController
#CrossOrigin
public class TeamResource {
#Autowired private TeamRepository teamRepository;
public TeamResource() {
}
...
//mapping methods
}
And if it relevant...
#Documented
#Constraint(validatedBy = TeamIdValidator.class)
#Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface TeamIdConstraint {
String message() default "Invalid team id!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Why on earth teamResource is null?
It is not being explicitly initialized with new anywhere else.
Already mentioned in the comments but declare a a bean like this
#Configuration
public class ValidationConfig{
#Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validator);
return methodValidationPostProcessor;
}
}
Remove all the #autowired, prefer constructor injection. (check the internet why).
And then, it makes no sense to inject a RestContoller to validator.
I have controller with method:
void save(#Validated(BookingForm.ValidationSave.class) #RequestBody BookingForm form, Errors result) {...}
and BookingForm with a field and my custom validator:
public class BookingForm {
public interface ValidationSave {}
public interface ValidationEstimate {}
....
#Future
private LocalDateTime when;
....
}
Future.java
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = FutureValidator.class)
#Documented
public #interface Future {
String message() default "{javax.validation.constraints.Future.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
I've expected that validator will valid my 'when' field but it does not.
When I removed the group from #Validated it works. Any ideas?
Works fine with #NotNull and others javax validators.
Updated:
FutureValidator.java
public class FutureValidator implements ConstraintValidator<Future, Temporal> {
#Override
public void initialize(Future constraintAnnotation) {
}
#Override
public boolean isValid(Temporal value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
LocalDate ld = LocalDate.from(value);
return ld.isAfter(LocalDate.now());
}
}
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");
}
}
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);
}
}
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());
}
...
}
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);
}
}
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;
}
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.
A solution with JUnit4 and Mockito:
#Import(LocalValidatorFactoryBean.class)
#RunWith(SpringRunner.class)
public class MyCustomValidatorTest {
#Autowired
private Validator validator;
#MockBean
private PersonRepository repository;
#Test
public void name_must_not_be_null() {
// given
when(repository.findByName(any())).thenReturn(Collection.emptyList());
Person person = new Person();
person.setName(null);
// when
Set<ConstraintViolation<Person>> violations = validator.validate(person);
// then
assertViolation(violations, "name", "Name must not be null");
}
}
You can test the validator stand alone and use reflection for inject the autowired attribute.
Constraint annotation:
#Target({ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EmailAlreadyExistsValidator.class)
public #interface EmailAlreadyExists {
String message() default "Email already exists in the database";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class EmailAlreadyExistsValidator implements
ConstraintValidator<EmailAlreadyExists, String> {
#Autowired
private UserRepository repository;
#Override
public void initialize(EmailAlreadyExists constraintAnnotation) {}
public boolean isValid(String email, ConstraintValidatorContext context) {
Optional<User> opUser = repository.findByEmail(email);
return (opUser.isEmpty());
}
}
Unit Test (ReflectionTestUtils do the magic):
public class EmailAlreadyExistsValidatorTest {
#Mock
private EmailAlreadyExists emailAlreadyExists;
#Mock
private ConstraintValidatorContext constraintValidatorContext;
#Mock
private UserRepository repository;
private EmailAlreadyExistsValidator validator;
#BeforeEach
public void beforeEach() {
MockitoAnnotations.openMocks(this);
validator = new EmailAlreadyExistsValidator();
validator.initialize(emailAlreadyExists);
ReflectionTestUtils.setField(validator, "repository", repository);
}
#Test
#DisplayName("Given an user with existent email then validation must fail")
public void isValid_existentPassword_mustFail() {
final String existentEmail = "testuser#test.com";
User savedUser = new User("1213443455",
"Test User",
existentEmail,
"12345",
new Date());
Optional<User> opUser = Optional.of(savedUser);
when(repository.findByEmail(anyString())).thenReturn(opUser);
assertFalse(validator.isValid(existentEmail,constraintValidatorContext));
}
}
It might be a bit late but I faced the same issue lately so I'll post how I solved the problem, as this could help other people.
The problem is basically that Hibernate's standard Validator implementation that you get by calling Validation.buildDefaultValidatorFactory().getValidator() does not know anything about Spring's application context so it cannot inject dependencies in your custom constraint validators.
In a Spring application the implementation of both the Validator and the ValidatorFactory interface is the class LocalValidatorFactoryBean, which can delegate to the ApplicationContext to instantiate constraint validators with dependencies injected.
What you need to do is
Instantiate your constraint validators with their (mocked, I presume) dependencies
Create your own ValidatorFactory that holds all the constraint validators from bulletpoint 1
Instantiate your Validator from such factory
This is the custom validator factory
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private final List<ConstraintValidator<?, ?>> customConstraintValidators;
public CustomLocalValidatorFactoryBean(List<ConstraintValidator<?, ?>> customConstraintValidators) {
this.customConstraintValidators = customConstraintValidators;
setProviderClass(HibernateValidator.class);
afterPropertiesSet();
}
#Override
protected void postProcessConfiguration(Configuration<?> configuration) {
super.postProcessConfiguration(configuration);
ConstraintValidatorFactory defaultConstraintValidatorFactory =
configuration.getDefaultConstraintValidatorFactory();
configuration.constraintValidatorFactory(
new ConstraintValidatorFactory() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
for (ConstraintValidator<?, ?> constraintValidator : customConstraintValidators) {
if (key.equals(constraintValidator.getClass())) //noinspection unchecked
return (T) constraintValidator;
}
return defaultConstraintValidatorFactory.getInstance(key);
}
#Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
defaultConstraintValidatorFactory
.releaseInstance(instance);
}
}
);
}
}
then in your test class you'd just do something like this:
class MyTestSuite {
private final PersonRepository mockPersonRepository = Mockito.mock(PersonRepository.class);
private final List<ConstraintValidator<?,?>> customConstraintValidators =
Collections.singletonList(new PersonNameMustBeUniqueValidator(mockPersonRepository));
private final ValidatorFactory customValidatorFactory =
new CustomLocalValidatorFactoryBean(customConstraintValidators);
private final Validator validator = customValidatorFactory.getValidator();
#Test
void myTestCase() {
// mock the dependency: Mockito.when(mockPersonRepository...)
Person p = new Person();
//setters omitted
Set<ConstraintViolation<?>> violations = validator.validate(p);
//assertions on the set of constraint violations
}
}
Hope that helps. You can check out this post of mine for more details: https://codemadeclear.com/index.php/2021/01/26/how-to-mock-dependencies-when-unit-testing-custom-validators/
I've implemented by overriding default Hibernate ConstraintValidatorFactory in my UnitTests
LocalValidatorFactoryBean localValidatorFactory = new LocalValidatorFactoryBean();
localValidatorFactory.setProviderClass(HibernateValidator.class);
localValidatorFactory.setConstraintValidatorFactory(new ConstraintValidatorFactoryImpl() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> arg0) {
T ret = super.getInstance(arg0);
if (ret instanceof UniqueEmailValidator) {
((UniqueEmailValidator) ret).setUserService(userService);
}
return ret;
}
});
localValidatorFactory.afterPropertiesSet();
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);
}
}