
Unit Test Bean Validation
- 2.8k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
unit-test-bean-validation is a developer-kit skill with JUnit 5 patterns for testing Jakarta Bean Validation constraints and custom validators without Spring.
About
Unit Test Bean Validation provides executable patterns for testing Jakarta Bean Validation and JSR-380 annotations with Hibernate Validator in isolation. It covers built-in constraints such as NotNull, Email, Min, Max, and Size, plus custom Constraint implementations, cross-field validation, and validation groups. Tests build a Validator once per class in BeforeEach using Validation.buildDefaultValidatorFactory and assert on ConstraintViolation property paths, messages, and invalid values. Examples include Maven test-scope dependencies for jakarta.validation-api, hibernate-validator, and AssertJ, a shared BaseValidationTest setup, valid and invalid case coverage, and parameterized tests for multiple inputs. Best practices stress null edge cases, exact violation counts, stateless custom validators, and keeping validation tests out of controller integration layers. Reference files document custom validators and advanced validation group patterns. The skill explicitly targets fast unit tests without Spring Boot context or database dependencies.
- JUnit 5 plus Hibernate Validator tests without Spring context.
- BaseValidationTest shared Validator factory setup pattern.
- AssertJ extraction of property paths, messages, and invalid values.
- Custom validator and cross-field validation reference guides.
- Parameterized tests and validation groups for conditional rules.
Unit Test Bean Validation by the numbers
- 2,799 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #314 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
unit-test-bean-validation capabilities & compatibility
- Capabilities
- validator factory setup in beforeeach · built in constraint valid and invalid case tests · property path and message assertion patterns · custom constraintvalidator test guidance · cross field validation test patterns · validation group and parameterized test support
- Use cases
- testing · api development
- Pricing
- Free
What unit-test-bean-validation says it does
Tests run in isolation without Spring context.
validator = Validation.buildDefaultValidatorFactory().getValidator();
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-bean-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.8k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I unit test @NotNull, @Email, and custom @Constraint validators on DTOs without spinning up Spring Boot?
Write JUnit 5 unit tests for Jakarta Bean Validation constraints and custom validators without starting a Spring context.
Who is it for?
Java backend developers writing fast validation unit tests for DTOs and custom constraint implementations.
Skip if: Skip for Spring MVC integration tests, database constraint checks, or non-JVM validation libraries.
When should I use this skill?
User writes bean validation tests, custom constraint validator tests, or JSR-380 DTO validation without Spring context.
What you get
JUnit tests asserting empty or sized violation sets with correct property paths, messages, and edge-case coverage.
- junit validation tests
- group coverage matrix
- constraint violation assertions
Files
Unit Testing Jakarta Bean Validation
Overview
This skill provides executable patterns for unit testing Jakarta Bean Validation annotations and custom validators using JUnit 5. Covers built-in constraints (@NotNull, @Email, @Min, @Max, @Size), custom @Constraint implementations, cross-field validation, and validation groups. Tests run in isolation without Spring context.
When to Use
- Writing unit tests for Jakarta Bean Validation or JSR-380 constraints
- Testing custom
@Constraintvalidators and constraint violation messages - Testing bean validation logic in DTOs and request objects
- Verifying cross-field validation (e.g., password matching)
- Testing conditional validation with validation groups
- Fast validation tests without Spring Boot context
Instructions
1. Add dependencies: Include jakarta.validation-api and hibernate-validator in test scope 2. Create base test class: Build Validator once in @BeforeEach using Validation.buildDefaultValidatorFactory() 3. Test valid cases first: Verify objects pass without violations 4. Test invalid cases: Assert constraint violations include correct property path and message 5. Extract violation details: Use getPropertyPath(), getMessage(), getInvalidValue() 6. Test custom validators: See references/custom-validators.md for patterns 7. Use parameterized tests: Test multiple inputs efficiently with @ParameterizedTest 8. Group validation tests: Use validation groups for conditional rules (see references/advanced-patterns.md)
Examples
Maven Setup
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>Common Test Setup
import jakarta.validation.*;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.path.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class BaseValidationTest {
protected Validator validator;
@BeforeEach
void setUpValidator() {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}Testing Basic Constraints
class UserDtoTest extends BaseValidationTest {
@Test
void shouldPassValidationWithValidUser() {
UserDto user = new UserDto("Alice", "alice@example.com", 25);
assertThat(validator.validate(user)).isEmpty();
}
@Test
void shouldFailWhenNameIsNull() {
UserDto user = new UserDto(null, "alice@example.com", 25);
assertThat(validator.validate(user))
.extracting(ConstraintViolation::getMessage)
.contains("must not be blank");
}
@Test
void shouldFailWhenEmailIsInvalid() {
UserDto user = new UserDto("Alice", "invalid-email", 25);
Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
assertThat(violations)
.extracting(ConstraintViolation::getPropertyPath)
.extracting(Path::toString)
.contains("email");
}
@Test
void shouldFailWhenAgeIsBelowMinimum() {
UserDto user = new UserDto("Alice", "alice@example.com", -1);
assertThat(validator.validate(user))
.extracting(ConstraintViolation::getMessage)
.contains("must be greater than or equal to 0");
}
@Test
void shouldFailWhenMultipleConstraintsViolated() {
UserDto user = new UserDto(null, "invalid", -5);
assertThat(validator.validate(user)).hasSize(3);
}
}Testing Custom Validators
For custom constraint patterns, see references/custom-validators.md:
- Creating
@Constraintannotations - Implementing
ConstraintValidator - Cross-field validation (password matching)
- Stateless validator best practices
Testing Validation Groups
For validation groups and parameterized tests, see references/advanced-patterns.md:
- Defining validation group interfaces
- Conditional validation with
groupsparameter @ParameterizedTestwith@ValueSourceand@CsvSource- Debugging failed validation tests
Best Practices
- Test both valid and invalid: Every constraint needs both passing and failing test cases
- Assert violation details: Verify property path, message, and constraint type
- Test edge cases: null, empty string, whitespace-only, boundary values
- Keep validators stateless: Custom validators must not maintain state
- Use clear messages: Constraint messages should be user-friendly
- Group related tests: Extend
BaseValidationTestto share validator setup - Test error messages: Ensure messages match requirements
Common Pitfalls
- Forgetting to test null values (most constraints ignore null by default)
- Not verifying the property path in constraint violations
- Testing validation at service/controller level instead of unit level
- Creating overly complex custom validators
- Missing
@NotNullfor mandatory fields combined with other constraints
Constraints and Warnings
- Null handling: Most constraints ignore null by default — combine
@NotNullwith other constraints for mandatory fields - Thread safety:
Validatorinstances are thread-safe and can be shared - Message localization: Test with different locales if i18n is required
- Cascading validation: Use
@Validon nested objects for recursive validation - Custom validators: Must be stateless and return
truefor null values - Test isolation: Validation unit tests should not depend on Spring context or database
Troubleshooting
ValidatorFactory not found: Ensure jakarta.validation-api and hibernate-validator are on test classpath.
Custom validator not invoked: Verify @Constraint(validatedBy = YourValidator.class) annotation is correct.
Null values pass validation: This is expected behavior — constraints ignore null unless @NotNull is present.
Wrong violation count: Use hasSize() to verify exact count, check all fields in the object.
Property path incorrect: Ensure the field, not the getter, has the constraint annotation.
References
- Jakarta Bean Validation Spec
- Hibernate Validator
- Custom validators and cross-field validation:
references/custom-validators.md - Validation groups and parameterized tests:
references/advanced-patterns.md
Advanced Validation Patterns Reference
Validation Groups
Defining Groups
public interface CreateValidation {}
public interface UpdateValidation {}
public interface AdminValidation {}Using Groups in DTOs
class UserDto {
@NotNull(groups = CreateValidation.class)
private String name;
@Min(value = 0, groups = {CreateValidation.class, UpdateValidation.class})
private int age;
}Testing Groups
class ValidationGroupsTest extends BaseValidationTest {
@Test
void shouldRequireNameOnlyDuringCreation() {
UserDto user = new UserDto(null, 25);
Set<ConstraintViolation<UserDto>> violations =
validator.validate(user, CreateValidation.class);
assertThat(violations)
.extracting(ConstraintViolation::getPropertyPath)
.extracting(Path::toString)
.contains("name");
}
@Test
void shouldAllowNullNameDuringUpdate() {
UserDto user = new UserDto(null, 25);
assertThat(validator.validate(user, UpdateValidation.class)).isEmpty();
}
@Test
void shouldValidateMultipleGroups() {
UserDto user = new UserDto("Alice", -5);
Set<ConstraintViolation<UserDto>> violations =
validator.validate(user, CreateValidation.class, UpdateValidation.class);
assertThat(violations).isNotEmpty();
}
}Parameterized Tests
Email Validation
class EmailValidationTest extends BaseValidationTest {
@ParameterizedTest
@ValueSource(strings = {
"user@example.com",
"john.doe+tag@example.co.uk",
"admin@subdomain.example.com"
})
void shouldAcceptValidEmails(String email) {
UserDto user = new UserDto("Alice", email);
assertThat(validator.validate(user)).isEmpty();
}
@ParameterizedTest
@ValueSource(strings = {
"invalid-email", "user@", "@example.com", "user name@example.com"
})
void shouldRejectInvalidEmails(String email) {
UserDto user = new UserDto("Alice", email);
assertThat(validator.validate(user)).isNotEmpty();
}
}Multiple Parameters
class RangeValidationTest extends BaseValidationTest {
@ParameterizedTest
@CsvSource({
"0, 100, true",
"-1, 100, false",
"0, 0, false",
"50, 100, true"
})
void shouldValidateRange(int min, int max, boolean shouldPass) {
RangeDto dto = new RangeDto(min, max);
var violations = validator.validate(dto);
assertThat(violations.isEmpty()).isEqualTo(shouldPass);
}
}Debugging Failed Tests
When Tests Fail
1. Check violation count: assertThat(violations).hasSize(n) 2. Inspect property path: violation.getPropertyPath().toString() 3. Verify message: violation.getMessage() 4. Check invalid value: violation.getInvalidValue()
@Test
void debugFailedValidation() {
UserDto user = new UserDto("", "invalid");
Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
// Debug output
violations.forEach(v -> System.out.println(
v.getPropertyPath() + ": " + v.getMessage()
));
assertThat(violations).hasSize(2);
}Common Issues
| Issue | Cause | Solution |
|---|---|---|
ConstraintViolation null | Object is valid or constraint doesn't fire | Check annotation parameters |
| Wrong property path | Wrong field annotated | Verify @Constraint(validatedBy=) |
| Null passes validation | Constraint allows null | Add @NotNull |
| Multiple violations | Multiple constraints fail | Use hasSize() to verify count |
Custom Validators Testing Reference
Creating Custom Constraints
Annotation Definition
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
String message() default "invalid phone number format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Validator Implementation
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
private static final String PHONE_PATTERN = "^\\d{3}-\\d{3}-\\d{4}$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // null handled by @NotNull
return value.matches(PHONE_PATTERN);
}
}Unit Test
class PhoneNumberValidatorTest extends BaseValidationTest {
@Test
void shouldAcceptValidPhoneNumber() {
Contact contact = new Contact("Alice", "555-123-4567");
assertThat(validator.validate(contact)).isEmpty();
}
@Test
void shouldRejectInvalidFormat() {
Contact contact = new Contact("Alice", "5551234567");
assertThat(validator.validate(contact))
.extracting(ConstraintViolation::getMessage)
.contains("invalid phone number format");
}
@Test
void shouldAllowNull() {
Contact contact = new Contact("Alice", null);
assertThat(validator.validate(contact)).isEmpty();
}
}Cross-Field Validation
Password Match Example
@PasswordsMatch
public class ChangePasswordRequest {
private String newPassword;
private String confirmPassword;
}
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordsMatch {
String message() default "passwords do not match";
Class<?>[] groups() default {};
}
public class PasswordMatchValidator
implements ConstraintValidator<PasswordsMatch, ChangePasswordRequest> {
@Override
public boolean isValid(ChangePasswordRequest value, ConstraintValidatorContext context) {
if (value == null) return true;
return value.getNewPassword().equals(value.getConfirmPassword());
}
}Test
class PasswordValidationTest extends BaseValidationTest {
@Test
void shouldPassWhenPasswordsMatch() {
var request = new ChangePasswordRequest("pass123", "pass123");
assertThat(validator.validate(request)).isEmpty();
}
@Test
void shouldFailWhenPasswordsDoNotMatch() {
var request = new ChangePasswordRequest("pass123", "different");
assertThat(validator.validate(request))
.extracting(ConstraintViolation::getMessage)
.contains("passwords do not match");
}
}Best Practices
- Keep validators stateless - no instance fields
- Return
truefornullvalues - let@NotNullhandle null checks - Provide clear error messages in annotations
- Test both valid and invalid cases
- Verify property path and message in assertions
Related skills
Forks & variants (1)
Unit Test Bean Validation has 1 known copy in the catalog totaling 20 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 20 installs
How it compares
Pick unit-test-bean-validation for constraint and group unit tests; pick integration-test skills for end-to-end API request validation across controllers.
FAQ
Which dependencies are required?
jakarta.validation-api and hibernate-validator in test scope, plus AssertJ for fluent assertions.
Why do null values sometimes pass?
Most constraints ignore null by default; combine @NotNull with other constraints for mandatory fields.
Are custom validators thread-safe?
Yes. Custom ConstraintValidator implementations must be stateless and return true for null values.