user1778855
user1778855

Reputation: 669

Spring unit test issue with Validator

I am trying to write unit test for a validator class that I have. So within my UniqueEmailValidator class, I injected a @Service component to check if it exist.

@AllArgsConstructor
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    
    private final AccountService accountService;
    
    @Override
    public void initialize(final UniqueEmail constraintAnnotation) {
    }

    @Override
    public boolean isValid(final String email, final ConstraintValidatorContext context) {
        return !this.accountService.findByEmail(email).isPresent();
    }
}

@Documented
@Target({TYPE, FIELD, ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    
    String message() default "{com.x.x.validator.UniqueEmail.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

And I tried to write unit test for this constraint validator.

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountValidatorTest {
    
    private static Validator validator;

    @BeforeClass
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    
    @Autowired
    private AccountService accountService;
        
    @Test
    public void shouldDetectDuplicatedEmailAddress() {
        
        User user = new User(); 
        // Setters omit
        
        // accountRepository.save(user);
                        
        Set<ConstraintViolation<AccountRegistrationForm>> violations = validator.validate(user);
        
        assertEquals(1, violations.size());
    }

}

How do I initialize the AccountService within the Validator class? It seem like it wasn't injected, hence, the null exception. Here's the trace.

javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.x.x.validator.UniqueEmailValidator.
    at org.hibernate.validator.internal.util.privilegedactions.NewInstance.run(NewInstance.java:43)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.run(ConstraintValidatorFactoryImpl.java:43)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.getInstance(ConstraintValidatorFactoryImpl.java:28)
    at org.hibernate.validator.internal.engine.constraintvalidation.ClassBasedValidatorDescriptor.newInstance(ClassBasedValidatorDescriptor.java:65)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager.createAndInitializeValidator(ConstraintValidatorManager.java:184)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager.getInitializedValidator(ConstraintValidatorManager.java:136)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:148)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:124)
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:55)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:380)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:169)
    at com.x.x.AccountValidatorTest.shouldDetectDuplicatedEmailAddress(AccountValidatorTest.java:95)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
Caused by: java.lang.InstantiationException: com.x.x.validator.UniqueEmailValidator
    at java.lang.Class.newInstance(Unknown Source)
    at org.hibernate.validator.internal.util.privilegedactions.NewInstance.run(NewInstance.java:40)
    ... 50 more
Caused by: java.lang.NoSuchMethodException: com.x.x.validator.UniqueEmailValidator.<init>()
    at java.lang.Class.getConstructor0(Unknown Source)
    ... 52 more

Thanks.

Upvotes: 4

Views: 5372

Answers (4)

ismayilkarimli
ismayilkarimli

Reputation: 485

I faced basically the same issue but with Mock testing and here's how I solved it:

  1. Here is how my custom constraint validation looks like:

Interface:

   @Target(ElementType.FIELD)
   @Retention(RetentionPolicy.RUNTIME)
   @Constraint(validatedBy = UniqueUsernameValidator.class)
   public @interface UniqueUsername {

    String message() default "This username is already in use";

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

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

   }

Implementation:

    @RequiredArgsConstructor
    public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {

        private final UserRepository repository;

        @Override
        public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
            return !repository.existsById(username);
        }
   }
  1. Annotate your test class with @SpringBootTest

  2. Set up MockMvc through Spring configuration:

    @ExtendWith(MockitoExtension.class)
    @SpringBootTest
    public class RegistrationTest {
    
        @Autowired
        private WebApplicationContext wac;
    
        private MockMvc mockMvc;
    
        @BeforeEach
        void setUp() {
            this.mockMvc = MockMvcBuilders
                    .webAppContextSetup(wac)
                    .alwaysDo(print())
                    .build();
        }
    
        // your tests
    
    }
    

Upvotes: 1

Jacob van Lingen
Jacob van Lingen

Reputation: 9537

There is a way to setup the test without adding a default constructor by providing your validator as context to the validator factory. You can do this by mocking a ConstraintValidatorFactory and returning a new UniqueEmailValidator instance when requested:

private static Validator validator;

@Autowired
private final AccountService accountService;

@BeforeClass
public static void setUp() {
    ConstraintValidatorFactory cvf = mock(ConstraintValidatorFactory.class);
    when(cvf.getInstance(UniqueEmailValidator.class)).thenReturn(new UniqueEmailValidator(accountService));

    validator = Validation.buildDefaultValidatorFactory()
        .usingContext()
        .constraintValidatorFactory(cvf)
        .getValidator();
}

...

Upvotes: 1

Yan Khonski
Yan Khonski

Reputation: 13083

You need to have a default non argument public constructor. If you need to pass a Spring component into your validator, you may face a problem in test.

I solved it by having a component which holds a service and has a static method to give the service.

Upvotes: 1

N00b Pr0grammer
N00b Pr0grammer

Reputation: 4647

The actual problem is deep down in the stack-trace that you've provided:

Caused by: java.lang.NoSuchMethodException: com.x.x.validator.UniqueEmailValidator.<init>()
at java.lang.Class.getConstructor0(Unknown Source)
... 52 more

This error message is thrown because there is some code that is trying to instantiate the UniqueEmailValidator class constructor without any parameters. The problem will be resolved by adding a default constructor to this class:

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    public UniqueEmailValidator() {
    }
    ...
}

Hope this helps!

Upvotes: 5

Related Questions