unit-test-bean-validation

安装量: 1.5K
排名: #1013

安装

npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-bean-validation
Unit Testing Bean Validation and Custom Validators
Overview
This skill provides patterns for unit testing Jakarta Bean Validation annotations and custom validator implementations using JUnit 5. It covers testing built-in constraints (@NotNull, @Email, @Min, @Max), creating custom validators, cross-field validation, validation groups, and parameterized testing scenarios.
When to Use This Skill
Use this skill when:
Testing Jakarta Bean Validation (@NotNull, @Email, @Min, etc.)
Testing custom @Constraint validators
Verifying constraint violation error messages
Testing cross-field validation logic
Want fast validation tests without Spring context
Testing complex validation scenarios and edge cases
Instructions
Add validation dependencies
Include jakarta.validation-api and hibernate-validator in your test classpath
Create a Validator instance
Use
Validation.buildDefaultValidatorFactory().getValidator()
in @BeforeEach
Test valid scenarios
Always test that valid objects pass validation without violations
Test each constraint separately
Create focused tests for individual validation rules
Extract violation details
Use assertions to verify property path, message, and invalid value
Test custom validators
Write dedicated tests for each custom constraint implementation
Use parameterized tests
Apply @ParameterizedTest for testing multiple invalid inputs efficiently
Test validation groups
Verify conditional validation based on validation groups
Examples
Setup: Bean Validation
Maven
<
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.junit.jupiter
</
groupId
>
<
artifactId
>
junit-jupiter
</
artifactId
>
<
scope
>
test
</
scope
>
</
dependency
>
<
dependency
>
<
groupId
>
org.assertj
</
groupId
>
<
artifactId
>
assertj-core
</
artifactId
>
<
scope
>
test
</
scope
>
</
dependency
>
Gradle
dependencies
{
implementation
(
"jakarta.validation:jakarta.validation-api"
)
testImplementation
(
"org.hibernate.validator:hibernate-validator"
)
testImplementation
(
"org.junit.jupiter:junit-jupiter"
)
testImplementation
(
"org.assertj:assertj-core"
)
}
Basic Pattern: Testing Validation Constraints
Setup Validator
import
jakarta
.
validation
.
Validator
;
import
jakarta
.
validation
.
ValidatorFactory
;
import
jakarta
.
validation
.
Validation
;
import
jakarta
.
validation
.
ConstraintViolation
;
import
org
.
junit
.
jupiter
.
api
.
BeforeEach
;
import
org
.
junit
.
jupiter
.
api
.
Test
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
*
;
class
UserValidationTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
ValidatorFactory
factory
=
Validation
.
buildDefaultValidatorFactory
(
)
;
validator
=
factory
.
getValidator
(
)
;
}
@Test
void
shouldPassValidationWithValidUser
(
)
{
User
user
=
new
User
(
"Alice"
,
"alice@example.com"
,
25
)
;
Set
<
ConstraintViolation
<
User
>
>
violations
=
validator
.
validate
(
user
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
@Test
void
shouldFailValidationWhenNameIsNull
(
)
{
User
user
=
new
User
(
null
,
"alice@example.com"
,
25
)
;
Set
<
ConstraintViolation
<
User
>
>
violations
=
validator
.
validate
(
user
)
;
assertThat
(
violations
)
.
hasSize
(
1
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"must not be blank"
)
;
}
}
Testing Individual Constraint Annotations
Test @NotNull, @NotBlank, @Email
class
UserDtoTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@Test
void
shouldFailWhenEmailIsInvalid
(
)
{
UserDto
dto
=
new
UserDto
(
"Alice"
,
"invalid-email"
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
dto
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getPropertyPath
)
.
extracting
(
Path
::
toString
)
.
contains
(
"email"
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"must be a valid email address"
)
;
}
@Test
void
shouldFailWhenNameIsBlank
(
)
{
UserDto
dto
=
new
UserDto
(
" "
,
"alice@example.com"
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
dto
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getPropertyPath
)
.
extracting
(
Path
::
toString
)
.
contains
(
"name"
)
;
}
@Test
void
shouldFailWhenAgeIsNegative
(
)
{
UserDto
dto
=
new
UserDto
(
"Alice"
,
"alice@example.com"
,
-
5
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
dto
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"must be greater than or equal to 0"
)
;
}
@Test
void
shouldPassWhenAllConstraintsSatisfied
(
)
{
UserDto
dto
=
new
UserDto
(
"Alice"
,
"alice@example.com"
,
25
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
dto
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
}
Testing @Min, @Max, @Size Constraints
class
ProductDtoTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@Test
void
shouldFailWhenPriceIsBelowMinimum
(
)
{
ProductDto
product
=
new
ProductDto
(
"Laptop"
,
-
100.0
)
;
Set
<
ConstraintViolation
<
ProductDto
>
>
violations
=
validator
.
validate
(
product
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"must be greater than 0"
)
;
}
@Test
void
shouldFailWhenQuantityExceedsMaximum
(
)
{
ProductDto
product
=
new
ProductDto
(
"Laptop"
,
1000.0
,
999999
)
;
Set
<
ConstraintViolation
<
ProductDto
>
>
violations
=
validator
.
validate
(
product
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"must be less than or equal to 10000"
)
;
}
@Test
void
shouldFailWhenDescriptionTooLong
(
)
{
String
longDescription
=
"x"
.
repeat
(
1001
)
;
ProductDto
product
=
new
ProductDto
(
"Laptop"
,
1000.0
,
longDescription
)
;
Set
<
ConstraintViolation
<
ProductDto
>
>
violations
=
validator
.
validate
(
product
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"size must be between 0 and 1000"
)
;
}
}
Testing Custom Validators
Create and Test Custom Constraint
// Custom constraint annotation
@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
{
}
;
}
// Custom 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 values handled by @NotNull
return
value
.
matches
(
PHONE_PATTERN
)
;
}
}
// Unit test for custom validator
class
PhoneNumberValidatorTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@Test
void
shouldAcceptValidPhoneNumber
(
)
{
Contact
contact
=
new
Contact
(
"Alice"
,
"555-123-4567"
)
;
Set
<
ConstraintViolation
<
Contact
>
>
violations
=
validator
.
validate
(
contact
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
@Test
void
shouldRejectInvalidPhoneNumberFormat
(
)
{
Contact
contact
=
new
Contact
(
"Alice"
,
"5551234567"
)
;
// No dashes
Set
<
ConstraintViolation
<
Contact
>
>
violations
=
validator
.
validate
(
contact
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"invalid phone number format"
)
;
}
@Test
void
shouldRejectPhoneNumberWithLetters
(
)
{
Contact
contact
=
new
Contact
(
"Alice"
,
"ABC-DEF-GHIJ"
)
;
Set
<
ConstraintViolation
<
Contact
>
>
violations
=
validator
.
validate
(
contact
)
;
assertThat
(
violations
)
.
isNotEmpty
(
)
;
}
@Test
void
shouldAllowNullPhoneNumber
(
)
{
Contact
contact
=
new
Contact
(
"Alice"
,
null
)
;
Set
<
ConstraintViolation
<
Contact
>
>
violations
=
validator
.
validate
(
contact
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
}
Testing Cross-Field Validation
Custom Multi-Field Constraint
// Custom constraint for cross-field validation
@Target
(
ElementType
.
TYPE
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
@Constraint
(
validatedBy
=
PasswordMatchValidator
.
class
)
public
@interface
PasswordsMatch
{
String
message
(
)
default
"passwords do not match"
;
Class
<
?
>
[
]
groups
(
)
default
{
}
;
Class
<
?
extends
Payload
>
[
]
payload
(
)
default
{
}
;
}
// Validator implementation
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
(
)
)
;
}
}
// Unit test
class
PasswordValidationTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@Test
void
shouldPassWhenPasswordsMatch
(
)
{
ChangePasswordRequest
request
=
new
ChangePasswordRequest
(
"oldPass"
,
"newPass123"
,
"newPass123"
)
;
Set
<
ConstraintViolation
<
ChangePasswordRequest
>
>
violations
=
validator
.
validate
(
request
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
@Test
void
shouldFailWhenPasswordsDoNotMatch
(
)
{
ChangePasswordRequest
request
=
new
ChangePasswordRequest
(
"oldPass"
,
"newPass123"
,
"differentPass"
)
;
Set
<
ConstraintViolation
<
ChangePasswordRequest
>
>
violations
=
validator
.
validate
(
request
)
;
assertThat
(
violations
)
.
extracting
(
ConstraintViolation
::
getMessage
)
.
contains
(
"passwords do not match"
)
;
}
}
Testing Validation Groups
Conditional Validation
@Target
(
ElementType
.
TYPE
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
interface
CreateValidation
{
}
@Target
(
ElementType
.
TYPE
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
interface
UpdateValidation
{
}
class
UserDto
{
@NotNull
(
groups
=
{
CreateValidation
.
class
}
)
private
String
name
;
@Min
(
value
=
1
,
groups
=
{
CreateValidation
.
class
,
UpdateValidation
.
class
}
)
private
int
age
;
}
class
ValidationGroupsTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@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
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
user
,
UpdateValidation
.
class
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
}
Testing Parameterized Validation Scenarios
import
org
.
junit
.
jupiter
.
params
.
ParameterizedTest
;
import
org
.
junit
.
jupiter
.
params
.
provider
.
ValueSource
;
class
EmailValidationTest
{
private
Validator
validator
;
@BeforeEach
void
setUp
(
)
{
validator
=
Validation
.
buildDefaultValidatorFactory
(
)
.
getValidator
(
)
;
}
@ParameterizedTest
@ValueSource
(
strings
=
{
"user@example.com"
,
"john.doe+tag@example.co.uk"
,
"admin123@subdomain.example.com"
}
)
void
shouldAcceptValidEmails
(
String
email
)
{
UserDto
user
=
new
UserDto
(
"Alice"
,
email
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
user
)
;
assertThat
(
violations
)
.
isEmpty
(
)
;
}
@ParameterizedTest
@ValueSource
(
strings
=
{
"invalid-email"
,
"user@"
,
"@example.com"
,
"user name@example.com"
}
)
void
shouldRejectInvalidEmails
(
String
email
)
{
UserDto
user
=
new
UserDto
(
"Alice"
,
email
)
;
Set
<
ConstraintViolation
<
UserDto
>
>
violations
=
validator
.
validate
(
user
)
;
assertThat
(
violations
)
.
isNotEmpty
(
)
;
}
}
Best Practices
Validate at unit test level
before testing service/controller layers
Test both valid and invalid cases
for every constraint
Use custom validators
for business-specific validation rules
Test error messages
to ensure they're user-friendly
Test edge cases
null, empty string, whitespace-only strings
Use validation groups
for conditional validation rules
Keep validator logic simple
- complex validation belongs in service tests
Common Pitfalls
Forgetting to test null values
Not extracting violation details (message, property, constraint type)
Testing validation at service/controller level instead of unit tests
Creating overly complex custom validators
Not documenting constraint purposes in error messages
Constraints and Warnings
Constraints ignore null by default
Except @NotNull, most constraints ignore null values; combine with @NotNull for mandatory fields
Validator is thread-safe
Validator instances can be shared across tests, but create new ones for isolation if needed
Message localization
Test with different locales if your application supports internationalization
Cascading validation
Use @Valid on nested objects to enable cascading validation
Performance consideration
Validation has overhead; don't over-validate in critical paths
Custom validators must be stateless
Validator implementations should not maintain state between invocations
Test in isolation
Validation tests should not depend on Spring context or database
Troubleshooting
ValidatorFactory not found
Ensure
jakarta.validation-api
and
hibernate-validator
are on classpath.
Custom validator not invoked
Verify
@Constraint(validatedBy = YourValidator.class)
is correctly specified.
Null handling confusion
By default, @NotNull checks null, other constraints ignore null (use @NotNull with others for mandatory fields). References Jakarta Bean Validation Spec Hibernate Validator Documentation Custom Constraints
返回排行榜