13

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

}
renanleandrof
  • 6,699
  • 9
  • 45
  • 67
  • Have you seen this - http://stackoverflow.com/questions/21878714/how-to-write-junit-test-case-with-spring-autowire. Your problem is the test setup not so much anything validator related. – Hardy Sep 04 '14 at 08:57

10 Answers10

10

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

  ...
}
Niki.Max
  • 163
  • 1
  • 5
9

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

}
Radouxca
  • 144
  • 1
  • 7
6

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

}
renanleandrof
  • 6,699
  • 9
  • 45
  • 67
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;

}
abhishekrvce
  • 185
  • 3
  • 9
3

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.

  • I haven't tested your code, but I have the impression your tests completely ignores that validator, because you don't set any validator in MockMvc. eg: MockMvcBuilders.standaloneSetup(getTestInstance()) .setValidator(new LocalValidatorFactoryBean()) .build(); – Franklin Dattein Jul 04 '18 at 05:19
  • @FranklinDattein My solution is only about the issue with Autowired. Maybe if I renamed test as integration test, it would make more scene to you. The validator will be called when you call the controller. – Hassan Mirzaee Jul 04 '18 at 06:25
  • 1
    Your solution doesn't provide expected solution. It still gives NullPointerException when going to Repository and still not mocking it. – Manish Bansal Oct 07 '19 at 15:21
3

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");
    }
}
Sergey Nemchinov
  • 1,348
  • 15
  • 21
1

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

        }
    
    }
Andre Luis
  • 41
  • 2
0

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

  1. Instantiate your constraint validators with their (mocked, I presume) dependencies
  2. Create your own ValidatorFactory that holds all the constraint validators from bulletpoint 1
  3. 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/

Bruno 82
  • 449
  • 2
  • 6
  • 15
0

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();
Olencha
  • 418
  • 2
  • 11
  • `ConstraintValidatorFactoryImpl.getInstance()` is `final` so this doesn't work. Not sure if that is a recent change. – E-Riz Oct 25 '22 at 13:30
-3

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);
    }
}
Niraj Sonawane
  • 10,225
  • 10
  • 75
  • 104
  • The OP has already autowired the beans, the question is about how to test validators with autowired beans – C.LS Jul 12 '19 at 15:43
  • downvoted - the question is about how to test validation and not how to implement it. – skryvets Dec 30 '19 at 19:56