Creating your own Annotation in Java and Testing them with Spock
Creating your own annotation in Java is really simple. Let's create one example. Say, we want to have a String that we are going to transform into a LocalDate and we want to make sure that String is set in the future. Let's call our annotation DateStringOnFutureConstraintAnnotation. It will take in a valid pattern, and a message that we will display if the date happens to be invalid. Our annotation will look like this when applied to a String.
Next we will need an interface that will define our annotation. Notice that this interface will not contain any logic. The logic will be placed in another class, and in order to let the interface know which logic it should use, we will have to define that with @Constraint(validatedBy = OurValidator.class). In this case our validator will be DateStringOnFutureValidator.
We don't really have to test this interface since it doesn't really contain any logic. Next we will have to define our validator: DateStringOnFutureValidator which implements ConstraintValidator. We will have to override the initialize method and isValid method for this. The initialize method will take the value of the pattern from our annotation, so that we could use it in our isValid method. For our is valid method, we will return true:
* if the date is blank or null
* if the date has a valid format and the value is set in the future.
Otherwise we will be returning false. Below is the sample code of what we want to achieve above.
So, how do we test this Validator on spock? All we have to do is create a new instance of DateStringOnFutureValidator and Mock ConstraintValidatorContext like so:
@DateStringOnFutureConstraintAnnotation( pattern = "yyyy-MM-d", message = "Invalid expiry date. Please make sure it's set in the future." ) private String expiryDate;
Next we will need an interface that will define our annotation. Notice that this interface will not contain any logic. The logic will be placed in another class, and in order to let the interface know which logic it should use, we will have to define that with @Constraint(validatedBy = OurValidator.class). In this case our validator will be DateStringOnFutureValidator.
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = DateStringOnFutureValidator.class) @Documented public @interface DateStringOnFutureConstraintAnnotation { String message() default "Invalid date. Date should be set on the future."; Class[] groups() default { }; Class[] payload() default { }; String pattern(); }
We don't really have to test this interface since it doesn't really contain any logic. Next we will have to define our validator: DateStringOnFutureValidator which implements ConstraintValidator. We will have to override the initialize method and isValid method for this. The initialize method will take the value of the pattern from our annotation, so that we could use it in our isValid method. For our is valid method, we will return true:
* if the date is blank or null
* if the date has a valid format and the value is set in the future.
Otherwise we will be returning false. Below is the sample code of what we want to achieve above.
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; public class DateStringOnFutureValidator implements ConstraintValidator{ private String pattern; @Override public void initialize( DateStringOnFutureConstraintAnnotation constraintAnnotation) { this.pattern = constraintAnnotation.pattern(); } @Override public boolean isValid(String dateString, ConstraintValidatorContext context) { if (dateString == null || dateString.trim().length() == 0) { return true; } try { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); LocalDate dateValue = LocalDate.parse(dateString, formatter); return dateValue.isAfter(LocalDate.now()); } catch (DateTimeParseException e) { return false; } } }
So, how do we test this Validator on spock? All we have to do is create a new instance of DateStringOnFutureValidator and Mock ConstraintValidatorContext like so:
DateStringOnFutureValidator validator = new DateStringOnFutureValidator() ConstraintValidatorContext constraintValidatorContext = Mock(ConstraintValidatorContext)Once we've done that setup, we can use it in our tests. Here is an example:
import spock.lang.Specification import spock.lang.Unroll import javax.validation.ConstraintValidatorContext import java.time.LocalDate; class DateStringOnFutureValidatorSpec extends Specification { DateStringOnFutureValidator validator = new DateStringOnFutureValidator() ConstraintValidatorContext constraintValidatorContext = Mock(ConstraintValidatorContext) def "validate should return false if the dateString is not set in the future."() { given: DateStringOnFutureConstraintAnnotation constraintAnnotation = Mock() constraintAnnotation.pattern() >> "yyyy-MM-d" String yesterday = LocalDate.now().minusDays(1).toString() when: validator.initialize(constraintAnnotation) boolean result = validator.isValid(yesterday, constraintValidatorContext) then: assert !result } def "validate should return true if the dateString is set in the future."() { given: DateStringOnFutureConstraintAnnotation constraintAnnotation = Mock() constraintAnnotation.pattern() >> "yyyy-MM-d" String yesterday = LocalDate.now().plusDays(1).toString() when: validator.initialize(constraintAnnotation) boolean result = validator.isValid(yesterday, constraintValidatorContext) then: assert result } @Unroll def "validate should return true if the dateString is #condition ."() { given: DateStringOnFutureConstraintAnnotation constraintAnnotation = Mock() constraintAnnotation.pattern() >> "yyyy-MM-d" when: validator.initialize(constraintAnnotation) boolean result = validator.isValid(dateValue, constraintValidatorContext) then: assert result where: condition | dateValue "null" | null "blank" | "" "space" | " " } }See! It's very easy and intuitive. Once all these steps are done, we can now use the new annotation we just created.
No comments :