К основному контенту

Unit-testing object validation when validator has DI

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:
  1. Declare CustomConstraintValidatorFactory that implements javax.validation.ConstraintValidatorFactory
  2. Override getInstance method and on facing your constraint validator class instantiate it
  3. Otherwise delegate validator construction to org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl
  4. Build validator factory and provide it your CustomConstraintValidatorFactory
  5. Build validator, using that factory...


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 manually
class 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.ConstraintValidatorFactoryImpl
public 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

Комментарии

Популярные сообщения из этого блога

Занимательные алгоритмы. Поиск цикла в односвязном списке

И снова про тараканов, которые иногда возникают в голове. Как-то раз, засыпая, я задумался на курьезными задачками из своей сферы деятельности (Lotus Notes), которые можно было бы задать на собеседовании, плавно перешел к воспоминаниям о своих первых собеседования, когда опыта работы еще не было. Опыт самих собеседований у меня не велик а места, где задавались действительно интересные задачи (а не задачки типа: написать сортировку массива любым известным способом) вообще равны одному - это ABBYY. Как минимум одна задачка в списке на знание и понимание классических алгоритмов, описанных в книге Дональда Кнута -  Искусство программирования .

Lotus Notes FAQ. 8/9 Eclipse. Как настроить уведомления о Sametime сообщениях

Н а написание данной "инструкцию" натолкнул мой коллега. Помню, первый раз сам долго искал, как отключить постоянно выпрыгивающие уведомления о новых сообщениях в Sametime. И так, речь идет о клиентах IBM Notes 8+ версии Standart (Eclipse based). Как настроить уведомления о Sametime сообщениях?