diff --git a/documentation/pom.xml b/documentation/pom.xml
index 31b005bb52..a6a454e5a3 100644
--- a/documentation/pom.xml
+++ b/documentation/pom.xml
@@ -34,7 +34,7 @@
true
true
- -Duser.language=en -Duser.country=US
+ --add-opens java.base/java.lang=ALL-UNNAMED -Duser.language=en -Duser.country=US
forbidden-allow-junit.txt
..
@@ -97,6 +97,11 @@
junit
test
+
+ org.easymock
+ easymock
+ test
+
org.assertj
diff --git a/documentation/src/main/asciidoc/ch06.asciidoc b/documentation/src/main/asciidoc/ch06.asciidoc
index 3b44e36e9e..479b6049c6 100644
--- a/documentation/src/main/asciidoc/ch06.asciidoc
+++ b/documentation/src/main/asciidoc/ch06.asciidoc
@@ -323,6 +323,61 @@ include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/CarTest.ja
----
====
+[[validator-dependency-testing]]
+==== Testing constraint validator with dependencies
+
+Some DI frameworks (e.g. Spring) are capable of injecting dependencies into constraint validator instance:
+
+[[example-person-with-checkcase]]
+.Hibernate Validator test utilities Maven dependency
+====
+[source, XML]
+[subs="verbatim,attributes"]
+----
+
+ org.hibernate.validator
+ hibernate-validator-test-utils
+ {hvVersion}
+ test
+
+----
+====
+
+.Defining the `@ZipCode` constraint annotation
+====
+[source, JAVA, indent=0]
+----
+include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java[tags=include]
+----
+====
+
+.Applying the `@ZipCode` constraint
+====
+[source, JAVA, indent=0]
+----
+include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java[tags=include]
+----
+====
+
+.Using injected dependency in a constraint validator
+====
+[source, JAVA, indent=0]
+----
+include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java[tags=include]
+----
+====
+
+Finally, <> demonstrates how validating a `Person` instance which calls custom mocked validator.
+
+[[example-using-validator-dependency]]
+.Validating objects with the `@ZipCode` constraint
+====
+[source, JAVA, indent=0]
+----
+include::{sourcedir}/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java[tags=field]
+----
+====
+
[[section-class-level-constraints]]
=== Class-level constraints
diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java
new file mode 100644
index 0000000000..3ef533f154
--- /dev/null
+++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/CustomValidatorWithDependencyTest.java
@@ -0,0 +1,56 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.mock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+
+import org.hibernate.validator.testutil.PreconfiguredValidatorsValidatorFactory;
+
+import org.junit.Test;
+
+@SuppressWarnings("unused")
+//tag::field[]
+public class CustomValidatorWithDependencyTest {
+
+ @Test
+ public void mockCustomValidatorWithDependency() {
+ ZipCodeValidator zipCodeValidator = mock( ZipCodeValidator.class );
+
+ expect( zipCodeValidator.isValid( eq( "1234" ), isA( ConstraintValidatorContext.class ) ) )
+ .andStubReturn( true );
+ zipCodeValidator.initialize( isA( ZipCode.class ) );
+
+ replay( zipCodeValidator );
+
+ ValidatorFactory validatorFactory = PreconfiguredValidatorsValidatorFactory.builder()
+ .defaultValidators( Map.of( ZipCodeValidator.class, zipCodeValidator ) )
+ .build();
+
+ Validator validator = validatorFactory.getValidator();
+
+ Person person = new Person( "1234" );
+
+ Set> constraintViolations = validator.validate( person );
+
+ assertEquals( 0, constraintViolations.size() );
+
+ verify( zipCodeValidator );
+ }
+}
+//end::field[]
diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java
new file mode 100644
index 0000000000..d0f6032fde
--- /dev/null
+++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/Person.java
@@ -0,0 +1,17 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+//tag::include[]
+package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency;
+
+public class Person {
+
+ @ZipCode
+ private String zipCode;
+
+ public Person(String zipCode) {
+ this.zipCode = zipCode;
+ }
+}
+//end::include[]
diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java
new file mode 100644
index 0000000000..62cb7d8b83
--- /dev/null
+++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCode.java
@@ -0,0 +1,35 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+//tag::include[]
+package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+//tag::include[]
+@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
+@Retention(RUNTIME)
+@Constraint(validatedBy = ZipCodeValidator.class)
+@Documented
+public @interface ZipCode {
+
+ String message() default "{org.hibernate.validator.referenceguide.chapter06." +
+ "customvalidatorwithdependency.ZipCode.message}";
+
+ Class>[] groups() default { };
+
+ Class extends Payload>[] payload() default { };
+}
+//end::include[]
diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java
new file mode 100644
index 0000000000..a0ef602801
--- /dev/null
+++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeRepository.java
@@ -0,0 +1,9 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency;
+
+public interface ZipCodeRepository {
+ boolean isExist(String zipCode);
+}
diff --git a/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java
new file mode 100644
index 0000000000..58efb2a8ff
--- /dev/null
+++ b/documentation/src/test/java/org/hibernate/validator/referenceguide/chapter06/customvalidatorwithdependency/ZipCodeValidator.java
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+//tag::include[]
+package org.hibernate.validator.referenceguide.chapter06.customvalidatorwithdependency;
+
+//end::include[]
+
+import jakarta.inject.Inject;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+//tag::include[]
+public class ZipCodeValidator implements ConstraintValidator {
+
+ @Inject
+ public ZipCodeRepository zipCodeRepository;
+
+ @Override
+ public boolean isValid(String zipCode, ConstraintValidatorContext constraintContext) {
+ if ( zipCode == null ) {
+ return true;
+ }
+
+ return zipCodeRepository.isExist( zipCode );
+ }
+}
+//end::include[]
diff --git a/test-utils/pom.xml b/test-utils/pom.xml
index 40bbacd520..cbee1b29ed 100644
--- a/test-utils/pom.xml
+++ b/test-utils/pom.xml
@@ -28,6 +28,14 @@
maven-jar-plugin
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-report-plugin
+
org.moditect
moditect-maven-plugin
@@ -60,5 +68,17 @@
assertj-core
provided
+
+
+
+ org.testng
+ testng
+ test
+
+
+ org.easymock
+ easymock
+ test
+
diff --git a/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java
new file mode 100644
index 0000000000..32c223e5ee
--- /dev/null
+++ b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactory.java
@@ -0,0 +1,66 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.validator.testutil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorFactory;
+
+public class PreconfiguredConstraintValidatorFactory implements ConstraintValidatorFactory {
+
+ private final Map, ConstraintValidator, ?>> defaultValidators;
+ private final ConstraintValidatorFactory delegated;
+
+ private PreconfiguredConstraintValidatorFactory(Builder builder) {
+ this.defaultValidators = builder.defaultValidators;
+ this.delegated = builder.delegated;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public > T getInstance(Class key) {
+ if ( defaultValidators.containsKey( key ) ) {
+ return (T) defaultValidators.get( key );
+ }
+
+ return delegated.getInstance( key );
+ }
+
+ @Override
+ public void releaseInstance(ConstraintValidator, ?> instance) {
+ delegated.releaseInstance( instance );
+ }
+
+ public static class Builder {
+
+ private ConstraintValidatorFactory delegated;
+ private final Map, ConstraintValidator, ?>> defaultValidators = new HashMap<>();
+
+ private Builder() {
+ }
+
+ public Builder defaultValidators(
+ Map, ConstraintValidator, ?>> validators) {
+ this.defaultValidators.putAll( validators );
+ return this;
+ }
+
+ public Builder delegated(
+ ConstraintValidatorFactory delegated) {
+ this.delegated = delegated;
+ return this;
+ }
+
+ public PreconfiguredConstraintValidatorFactory build() {
+ return new PreconfiguredConstraintValidatorFactory( this );
+ }
+ }
+}
diff --git a/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java
new file mode 100644
index 0000000000..3b54d0d510
--- /dev/null
+++ b/test-utils/src/main/java/org/hibernate/validator/testutil/PreconfiguredValidatorsValidatorFactory.java
@@ -0,0 +1,112 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.validator.testutil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jakarta.validation.ClockProvider;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorFactory;
+import jakarta.validation.MessageInterpolator;
+import jakarta.validation.ParameterNameProvider;
+import jakarta.validation.TraversableResolver;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorContext;
+import jakarta.validation.ValidatorFactory;
+
+/**
+ * This class provides useful functions to create {@code ValidatorFactory} with preconfigured validators to test Bean
+ * validation without creation of custom validator instances.
+ *
+ * @author Attila Hajdu
+ */
+@SuppressWarnings("rawtypes")
+public class PreconfiguredValidatorsValidatorFactory implements ValidatorFactory {
+ private final Map, ConstraintValidator, ?>> defaultValidators;
+ private final ValidatorFactory delegated;
+
+ private PreconfiguredValidatorsValidatorFactory(Builder builder) {
+ this.defaultValidators = builder.defaultValidators;
+
+ ValidatorFactory defaultValidationFactory = Validation.buildDefaultValidatorFactory();
+ ConstraintValidatorFactory wrappedConstraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder()
+ .delegated( defaultValidationFactory.getConstraintValidatorFactory() )
+ .defaultValidators( this.defaultValidators ).build();
+
+ this.delegated = Validation.byDefaultProvider().configure()
+ .constraintValidatorFactory( wrappedConstraintValidatorFactory )
+ .buildValidatorFactory();
+
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public Validator getValidator() {
+ return delegated.getValidator();
+ }
+
+ @Override
+ public ValidatorContext usingContext() {
+ return delegated.usingContext();
+ }
+
+ @Override
+ public MessageInterpolator getMessageInterpolator() {
+ return delegated.getMessageInterpolator();
+ }
+
+ @Override
+ public TraversableResolver getTraversableResolver() {
+ return delegated.getTraversableResolver();
+ }
+
+ @Override
+ public ConstraintValidatorFactory getConstraintValidatorFactory() {
+ return delegated.getConstraintValidatorFactory();
+ }
+
+ @Override
+ public ParameterNameProvider getParameterNameProvider() {
+ return delegated.getParameterNameProvider();
+ }
+
+ @Override
+ public ClockProvider getClockProvider() {
+ return delegated.getClockProvider();
+ }
+
+ @Override
+ public T unwrap(Class type) {
+ return delegated.unwrap( type );
+ }
+
+ @Override
+ public void close() {
+ delegated.close();
+ }
+
+ public static class Builder {
+
+ private Builder() {
+ }
+
+ private final Map, ConstraintValidator, ?>> defaultValidators = new HashMap<>();
+
+ public Builder defaultValidators(
+ Map, ConstraintValidator, ?>> defaultValidators) {
+ this.defaultValidators.putAll( defaultValidators );
+ return this;
+ }
+
+ public PreconfiguredValidatorsValidatorFactory build() {
+ return new PreconfiguredValidatorsValidatorFactory( this );
+ }
+ }
+}
diff --git a/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java b/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java
new file mode 100644
index 0000000000..9e83dbd98a
--- /dev/null
+++ b/test-utils/src/test/java/org/hibernate/validator/testutil/PreconfiguredConstraintValidatorFactoryTest.java
@@ -0,0 +1,74 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.validator.testutil;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.easymock.EasyMock.*;
+
+import java.util.Map;
+
+import jakarta.validation.ConstraintValidatorFactory;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class PreconfiguredConstraintValidatorFactoryTest {
+
+ private ConstraintValidatorFactory delegatedConstraintValidatorFactory;
+
+ @BeforeMethod
+ public void setUp() {
+ delegatedConstraintValidatorFactory = createMock( ConstraintValidatorFactory.class );
+ }
+
+ @Test
+ public void testGetInstanceWithPreconfiguredValidator() {
+ CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator();
+
+ PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder()
+ .delegated( delegatedConstraintValidatorFactory )
+ .defaultValidators( Map.of( CountValidationCallsValidator.class, constraintValidator ) )
+ .build();
+
+ assertThat( constraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) )
+ .isEqualTo( constraintValidator );
+ }
+
+ @Test
+ public void testGetInstanceWithDefaultValidator() {
+ CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator();
+
+ expect( delegatedConstraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) ).andReturn( constraintValidator );
+
+ PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder()
+ .delegated( delegatedConstraintValidatorFactory )
+ .build();
+
+ replay( delegatedConstraintValidatorFactory );
+
+ assertThat( constraintValidatorFactory.getInstance( CountValidationCallsValidator.class ) )
+ .isEqualTo( constraintValidator );
+
+ verify( delegatedConstraintValidatorFactory );
+ }
+
+ @Test
+ public void testReleaseInstance() {
+ CountValidationCallsValidator constraintValidator = new CountValidationCallsValidator();
+
+ delegatedConstraintValidatorFactory.releaseInstance( constraintValidator );
+
+ PreconfiguredConstraintValidatorFactory constraintValidatorFactory = PreconfiguredConstraintValidatorFactory.builder()
+ .delegated( delegatedConstraintValidatorFactory )
+ .build();
+
+ replay( delegatedConstraintValidatorFactory );
+
+ constraintValidatorFactory.releaseInstance( constraintValidator );
+
+ verify( delegatedConstraintValidatorFactory );
+ }
+}