Unit 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.
/plugin marketplace add giuseppe-trisciuoglio/developer-kit/plugin install developer-kit@giuseppe.trisciuoglioThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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).
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.