
Unit Test Bean Validation
- 20 installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit-claude-code
This is a copy of unit-test-bean-validation by giuseppe-trisciuoglio - installs and ranking accrue to the original listing.
Helps with testing & qa tasks.
About
unit-test-bean-validation is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- unit-test-bean-validation
- Testing & QA
- AI-coding skill
Unit Test Bean Validation by the numbers
- 20 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit-claude-code --skill unit-test-bean-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 318 |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit-claude-code ↗ |
What it does
Helps with testing & qa tasks.
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