The Java Validation API provides a very convenient API for input validation. It is part of the JavaEE spec, nevertheless it can be used within a Spring application too. Addressing the whole functioning of the Java Validation API is beyond the scope of this post, we will focus instead on how to unit test the validation layer. In this regard, we will show how to mock dependencies that might be injected in your custom constraint validators.
The project
When using Spring Boot all you have to do is add the spring-boot-starter-validation dependency in your project’s pom.xml. The Hibernate Validator will be the chosen implementation of the Java Validation API, that’s by far the most popular and widely used implementation.
The model
Let’s start with a very basic example. To keep things simple, we’ll have only one model class called Employee that has three fields:
- an
id
field (the primary key) - a
fullName
field - a
socialSecurityNumber
field
The id
field has no constraint on it because we assume that the primary key value will be generated from a sequence when a new entity is persisted, hence we can accept an Employee with null
primary key as input.
The two fields of type String have the @NotEmpty constraint because they are mandatory.
The service layer
We have one service interface with just one method to save (either insert or update) employees in a persistence store, updating an existing employee if the provided employee’s id
is not null
and can be found in the persistence store, inserting a new employee otherwise. The persistence store could be a SQL database, noSQL database, or anything else: that’s irrelevant for our purpose. The following is the interface:
We are not even going to provide an implementation, because we’re interested in testing the validation layer, and the validation annotations are already provided in the interface. Any implementing class is supposed to repeat those annotations and add @Validated at the class level to trigger validation.
Test suite setup
Let’s analyze our test suite now. The EmployeeService is mocked to return the same Employee instance provided as argument with the id
field generated from a sequence if necessary.
In a Spring application any managed bean implementing the EmployeeService interface annotated with @Validated at class level will be automatically wrapped in a proxy. Such proxy will be responsible for calling the validation API, with almost no effort from the programmer.
Since we are interested in unit testing, there’s no Spring application context available, hence we must manually instantiate a Validator and invoke the appropriate methods. The javax.validation.Validator interface represents the entry point for the Validation API. As per API specifications its implementations must be thread-safe, hence we can use the same Validator across the whole test suite.
In our test suite the Validator will be an instance of ValidatorImpl, which is Hibernate’s default implementation.
In a Spring Boot project the Validator implementation will be an instance of LocalValidatorFactoryBean, provided by Spring Boot’s autoconfiguration. This can be easily tested:
This particular Validator implementation can inject @Autowired dependencies in the constraint validators by delegating to Spring’s ApplicationContext to resolve dependencies. Anyway, for the first part of this post we are only testing validations that don’t require any dependency injection, as we are only going to validate against standard constraints like @NotNull and @NotEmpty.
In our test suite we are reproducing as closely as possible the same behaviour we’d have in our Spring application: we’re wrapping our mocked EmployeeService in a proxy that uses the Validator to get all possible constraint violations on the called method’s args. If any constraint is violated a javax.validation.ConstraintViolationException wrapping a Set of ConstraintViolations will be thrown. If no exception is thrown then the called method will execute normally.
Unit testing the validation layer
As we can see calling the save(Employee)
method with a null
arg will result in a ConstraintViolationException being thrown, with the violated constraint being the @NotNull in the method signature.
If the arg employee is not null
but its fullName
field is null
or empty, a ConstraintViolationException will be thrown due to the @NotEmpty constraint being violated.
The same applies when the socialSecurityNumber
field is null
or empty.
Creating a custom validation constraint
Now let’s try to add a custom constraint at class level. We’ll create a custom constraint annotation @UniqueSsn to be placed on the Employee class and a custom constraint validator UniqueSsnValidator.
So far the validator does nothing, as its isValid(Employee, ConstraintValidatorContext)
method will always return true
. If we run our test suite again all tests will still pass, and we can see from the logs that the custom validator is called: our Validator can instantiate the UniqueSsnValidator class using the default implicit no-args constructor and the validation is performed.
16:08:53.117 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - ValidationMessages not found.
16:08:53.118 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - ContributorValidationMessages not found.
16:08:53.119 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - org.hibernate.validator.ValidationMessages found.
16:08:53.119 [main] DEBUG com.codemadeclear.mockvalidatordependencies.validation.validators.UniqueSsnValidator - Inside the custom constraint validator…
The problem: @Autowired dependency not available in a custom ConstraintValidator
Now let’s make things a bit more interesting. The custom constraint validator is pretty useless in its current implementation. Let’s inject a dependency on a EmployeeValidationService:
Again, we’re not providing any implementation because all services will be mocked, but let’s give a quick overview of what this EmployeeValidationService would do in a real-world scenario:
- The EmployeeValidationService would lookup the persistence store querying the employees by the
socialSecurityNumber
field. - Since the ssn is unique, the query would return at most one employee.
- the
isSsnTaken(Employee)
method would return:false
if no employee is found by ssn, which means that there’s still no employee in the database with the same ssn as the employee being validatedfalse
if an employee is found by ssn but theid
is the same as theid
of the employee being validated, which means we’re just updating an employee without changing his/her own ssntrue
if an employee is found by ssn and theid
is different from theid
of the employee being validated, which means that we’re trying to duplicate the same ssn across two different employees in the database (either by inserting a new employee or updating an existing one)
The purpose of the validation is to prevent inserts or updates that would lead to data corruption like duplicated ssn (as in this case). But of course it’s not possible to implement a validator for such a constraint without a dependency on some service that can query the persistence store, because the constraint definition itself is strictly inherent to the database’s content.
Unfortunately, if we try to run our tests now most of them will fail with a stacktrace like this:
org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <javax.validation.ConstraintViolationException> but was: <javax.validation.ValidationException>
…
Caused by: javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.codemadeclear.mockvalidatordependencies.validation.validators.UniqueSsnValidator.
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)
at org.hibernate.validator.internal.engine.constraintvalidation.ClassBasedValidatorDescriptor.newInstance(ClassBasedValidatorDescriptor.java:84)
Our ValidatorImpl instance has an inner ConstraintValidatorFactory that cannot find a no args constructor to instantiate our constraint validator and doesn’t know how to use the @Autowired annotated constructor.
Using field injection will not solve the problem. If we refactor the UniqueSsnValidator class this way:
Now the tests will fail with a stacktrace like this:
16:30:23.997 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - ValidationMessages not found.
16:30:24.002 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - ContributorValidationMessages not found.
16:30:24.014 [main] DEBUG org.hibernate.validator.resourceloading.PlatformResourceBundleLocator - org.hibernate.validator.ValidationMessages found.
16:30:24.030 [main] DEBUG com.codemadeclear.mockvalidatordependencies.validation.validators.UniqueSsnValidator - Inside the custom constraint validator…
org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <javax.validation.ConstraintViolationException> but was: <javax.validation.ValidationException>
…
Caused by: javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.
…
Caused by: java.lang.NullPointerException
at com.codemadeclear.mockvalidatordependencies.validation.validators.UniqueSsnValidator.isValid(UniqueSsnValidator.java:25)
at com.codemadeclear.mockvalidatordependencies.validation.validators.UniqueSsnValidator.isValid(UniqueSsnValidator.java:12)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:180)
… 86 more
Now the constraint validator is instantiated via implicit no-args constructor but the dependency is still not being injected and we’re getting a NPE because the dependency is null
.
The solution: make your own Validator factory
The solution is to create a custom ValidatorFactory that can simulate Spring’s dependency injection.
Luckily we don’t need to mock the entire Spring application context’s functioning, we just need to manually inject the dependencies in the constraint validators we are testing.
We’ll create a class called CustomLocalValidatorFactoryBean which takes a List of ConstraintValidators as constructor argument, then we’ll instruct our factory to instantiate a Validator that uses those constraint validators when needed.
We can extend Spring’s LocalValidatorFactoryBean and leverage a built-in customization hook inside that class, namely the postProcessConfiguration()
method, which is called towards the end of the afterPropertiesSet()
method. The method’s documentation states
/**
* Post-process the given Bean Validation configuration,
* adding to or overriding any of its settings.
* <p>Invoked right before building the {@link ValidatorFactory}.
* @param configuration the Configuration object, pre-populated with
* settings driven by LocalValidatorFactoryBean's properties
*/
All we need to do is override the default configuration by decorating the default ConstraintValidatorFactory: whenever an instance of ConstraintValidator is required, the list of custom ConstraintValidators will be searched first. If any applicable instance is found it’ll be returned, otherwise the standard instantiation process via no-args constrctor will take place. Of course, since there’s no Spring ApplicationContext to call the afterPropertiesSet()
method, we need to manually call such method in the constructor.
Now we can use our custom ValidatorFactory in the test suite to get a Validator that knows how to get constraint validators with the desired (mocked) dependencies. The EmployeeValidationService is by default mocked to return false
when isSsnTaken(Employee)
is called, so that the validation will pass. Of course the mock can be overridden at the test case level if needed.
Now we can successfully test what happens when trying to save an Employee that has apparently all valid data but his/her ssn is already taken. As you can see, at the beginning of the test case the validation service is mocked to return true
when isSsnTaken(Employee)
is called. Our custom constraint validator will then return false
when the employee is validated and this will lead to a ConstraintValidationException being thrown because the @UniqueSsn constraint was violated.
Conclusion
We have seen how to unit test constraint validators that have @Autowired dependencies in a Spring Boot application. We can mock those dependencies without the need to start the entire Spring application context, thus keeping our tests fast and providing better isolation.
The example project can be downloaded from GitLab.