tessl i github:giuseppe-trisciuoglio/developer-kit --skill unit-test-bean-validationUnit testing Jakarta Bean Validation (@Valid, @NotNull, @Min, @Max, etc.) with custom validators and constraint violations. Test validation logic without Spring context. Use when ensuring data integrity and validation rules are correct.
Test validation annotations and custom validator implementations using JUnit 5. Verify constraint violations, error messages, and validation logic in isolation.
Use this skill when:
<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>dependencies {
implementation("jakarta.validation:jakarta.validation-api")
testImplementation("org.hibernate.validator:hibernate-validator")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}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");
}
}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();
}
}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");
}
}// 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();
}
}// 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");
}
}@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();
}
}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();
}
}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).
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.