Summary
Unit test object validation when validator(s) has a dependency.For instance, we have some custom field and cross-field validators. Want to test their combination. Additionally some of validators have dependencies, injected through constructor or setters. You're not using property injection, right?
Shortcut
If you are just searching for an answer, here's the fast way:- Declare CustomConstraintValidatorFactory that implements javax.validation.ConstraintValidatorFactory
- Override getInstance method and on facing your constraint validator class instantiate it
- Otherwise delegate validator construction to org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl
- Build validator factory and provide it your CustomConstraintValidatorFactory
- Build validator, using that factory...
Go to demo project on GitHub for details: https://github.com/MrArtemAA/blog-demos/blob/master/test-validator-with-injection/src/test/java/ru/artemaa/demo/ObjectToValidateUnitTest.java
Now we'll go through all the steps.
Set up
- Spring boot application
- javax.validation
- validated object
@ConsistentProperties public class ObjectToValidate { private String property; private String anotherProperty; //getters and setters }
- constraint
@Constraint(validatedBy = ConsistentPropertiesValidator.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ConsistentProperties { String message() default "Another Property is required"; Class[] groups() default { }; Class[] payload() default { }; }
- validator with dependency
public class ConsistentPropertiesValidator implements ConstraintValidator{ private final Config config; public ConsistentPropertiesValidator(Config config) { this.config = config; } public boolean isValid(ObjectToValidate obj, ConstraintValidatorContext context) { if (obj == null) { return true; } boolean shouldHaveAnotherProperty = config.getPropertyValues().contains(obj.getProperty()); return !(shouldHaveAnotherProperty && StringUtils.isEmpty(obj.getAnotherProperty())); } }
@SpringBootTest?
Ok, you say, we have Spring boot, let's create a test with @SpringBootTest. Indeed, why not:@SpringBootTest class ObjectToValidateSpringBootTest { @Autowired private Validator validator; @Test void validate() { ObjectToValidate objectToValidate = new ObjectToValidate(); objectToValidate.setProperty("value"); Set<ConstraintViolation<ObjectToValidate>> violations = validator.validate(objectToValidate); assertTrue(violations.isEmpty()); objectToValidate.setProperty("value1"); violations = validator.validate(objectToValidate); assertFalse(violations.isEmpty()); ConstraintViolation<ObjectToValidate> violation = violations.iterator().next(); assertEquals(ConsistentProperties.class, violation.getConstraintDescriptor().getAnnotation().annotationType()); objectToValidate.setProperty("value1"); objectToValidate.setAnotherProperty("another value"); violations = validator.validate(objectToValidate); assertTrue(violations.isEmpty()); } }
Let's run it several times:
What? Around 400 ms for a single test? In an empty application? Spring builds the whole context to run our test. It was told to do so with @SpringBootTest annotation. Imagine an application with a number of components, integrations, etc. It's unacceptable.
Code dive-in
It there other way? Let's create a unit test, and obtain/build javax.validation.Validator object manuallyclass ObjectToValidateFailingUnitTest { private static Validator validator; @BeforeAll static void setUpTest() { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); validator = validatorFactory.usingContext().getValidator(); } @Test void validate() { ObjectToValidate objectToValidate = new ObjectToValidate(); objectToValidate.setProperty("value"); Set<ConstraintViolation<ObjectToValidate>> violations = validator.validate(objectToValidate); assertTrue(violations.isEmpty()); objectToValidate.setProperty("value1"); violations = validator.validate(objectToValidate); assertFalse(violations.isEmpty()); ConstraintViolation<ObjectToValidate> violation = violations.iterator().next(); assertEquals(ConsistentProperties.class, violation.getConstraintDescriptor().getAnnotation().annotationType()); objectToValidate.setProperty("value1"); objectToValidate.setAnotherProperty("another value"); violations = validator.validate(objectToValidate); assertTrue(violations.isEmpty()); } }On running this test and exception occurs: java.lang.NoSuchMethodException: ConsistentPropertiesValidator.<init>() . No surprise, actually: Hibernate validator, which is a reference implementation, doesn't know how to build our custom validator. Let's make a code dive-in and figure out what could be done.
Well, it's not actually a deep dive. Brief stack trace analysis will give us the answer.
javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: ConsistentPropertiesValidator. at org.hibernate.validator.internal.util.privilegedactions.NewInstance.run(NewInstance.java:44) at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.run(ConstraintValidatorFactoryImpl.java:43) at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.getInstance(ConstraintValidatorFactoryImpl.java:28) ...Let's stop right here and explore getInstance method of org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.
public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory { @Override public final <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) { return run( NewInstance.action( key, "ConstraintValidator" ) ); } ... }Overridden getInstance method is declared final, it means we can't override it by simply creating a subclass of org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.
The solution
What we can do, is to create custom ConstraintValidatorFactory implementing javax.validation.ConstraintValidatorFactory. On facing our constraint validator class or classes we'll instantiate them, otherwise delegate validator construction to org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImplpublic class CustomConstraintValidatorFactory implements ConstraintValidatorFactory { private final ConstraintValidatorFactory hibernateConstraintValidatorFactory = new ConstraintValidatorFactoryImpl(); @Override public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) { if (ConsistentPropertiesValidator.class.equals(key)) { //noinspection unchecked return (T) new ConsistentPropertiesValidator(createTestConfig()); } return hibernateConstraintValidatorFactory.getInstance(key); } @Override public void releaseInstance(ConstraintValidator<?, ?> instance) { hibernateConstraintValidatorFactory.releaseInstance(instance); } }Last step is to configure javax.validation.ValidatorFactory with our CustomConstraintValidatorFactory
ValidatorFactory validatorFactory = Validation.byDefaultProvider().configure() .constraintValidatorFactory(new CustomConstraintValidatorFactory()) .buildValidatorFactory(); validator = validatorFactory.usingContext().getValidator();
Let's run it and compare the time:



Almost 4 times faster! Much better now.
Conclusion
Using raw @SpringBootTest to ensure DI might cost you a lot in terms of test run time. But pure validation (Hibernate validation) is not able to construct your validator, if it doesn't have default constructor. Even if you add a default constructor, got to find a way to set up validator with the dependency. Creating custom ConstraintValidatorFactory is quite a simple way to reduce test run time and make your validator be constructed properly.Anyway, is there a way to make Spring do all the work and still be quite fast? Well, yes. Stay tuned, we'll do it in the next post.
Full demo code could be found on GitHub: https://github.com/MrArtemAA/blog-demos/tree/master/test-validator-with-injection
Комментарии
Отправить комментарий