java - 使用 JUnit 5 测试自定义约束注解
问题描述
实现自定义约束注释,比如@MySize
需要我用单元测试来测试它,看看它是否正常工作:
public class MySizeTest {
@Test
public void noMinMax() {
Dummy dummy = new Dummy();
// some asserts or so
dummy.setMyField("");
dummy.setMyField(null);
dummy.setMyField("My text");
}
@Test
public void onlyMin() {
// change @MySize to have min: @MySize(min = 1)
... how?
... then test with some setMyField:
Dummy dummy = new Dummy();
// some asserts or so
dummy.setMyField("");
dummy.setMyField(null);
dummy.setMyField("My text");
}
@Test
public void onlyMax() {
// change @MySize to have max: @MySize(max = 50)
...
}
@Test
public void bothMinMax() {
// change @MySize to have min and max: @MySize(min = 1, max = 50)
...
}
private class Dummy {
@MySize()
String myField;
public String getMyField() {
return myField;
}
public void setMyField(String myField) {
this.myField = myField;
}
}
}
我认为这必须通过反射来完成,但我不知道如何。
解决方案
基本上不必使用反射只需创建一个Validator
实例并将其用于验证。
例如:
当注释为:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyValidator.class)
public @interface MyAnnotation {
String message() default "Invalid value (it must be foo)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
相关的验证器是:
public class MyValidator implements ConstraintValidator<MyAnnotation, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (null == s) return true;
return "foo".equalsIgnoreCase(s);
}
}
那么测试应该是这样的:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyValidatorTest {
private Validator validator;
@BeforeAll
void init() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
private static class TestObject {
@MyAnnotation
private String testField;
TestObject() {
this(null);
}
TestObject(String value) {
testField = value;
}
public String getTestField() {
return testField;
}
public void setTestField(String testField) {
this.testField = testField;
}
}
@Test
void shouldValidForNullValue() {
var obj = new TestObject();
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
}
@Test
void shouldValidForFooValue() {
var obj = new TestObject("foo");
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
}
@Test
void shouldInvalidForBarValue() {
var obj = new TestObject("bar");
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertEquals(1, violations.size());
}
}
更新 (2020.05.21.) - 使用属性和 AnnotationFactory
根据评论,我更新了我的答案。如果您只想测试验证逻辑,那么只需创建一个Annotation
实例并调用isValid
返回的方法true
或false
Hibernate Validator 提供AnnotationFactory.create(...)
了制作注解实例的方法。之后,您可以在测试用例中创建自定义验证器和调用initialize
和方法的实例。isValid
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyHasAttributesValidator.class)
public @interface MyAnnotationHasAttributes {
String message() default "Invalid value (it must be foo)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int attributeOne() default 10;
int attributeTwo() default 20;
}
相关验证器:
public class MyHasAttributesValidator implements ConstraintValidator<MyAnnotationHasAttributes, String> {
private MyAnnotationHasAttributes ann;
@Override
public void initialize(MyAnnotationHasAttributes constraintAnnotation) {
ann = constraintAnnotation;
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (null == s) return true;
return s.length() >= ann.attributeOne() && s.length() < ann.attributeTwo();
}
}
和修改后的测试(断言失败):
public class HasAttributeValidatorTest {
private MyAnnotationHasAttributes createAnnotation(Integer one, Integer two) {
final Map<String, Object> attrs = new HashMap<>();
if (null != one) {
attrs.put("attributeOne", one);
}
if (null != two) {
attrs.put("attributeOne", two);
}
var desc = new AnnotationDescriptor.Builder<>(MyAnnotationHasAttributes.class, attrs).build();
return AnnotationFactory.create(desc);
}
@ParameterizedTest
@MethodSource("provideValues")
void testValidator(Integer one, Integer two, String input, boolean expected) {
MyAnnotationHasAttributes ann = createAnnotation(one, two);
MyHasAttributesValidator validator = new MyHasAttributesValidator();
validator.initialize(ann);
var result = validator.isValid(input, null);
Assertions.assertEquals(expected, result, String.format("Validation must be %s but found: %s with params: %d, %d, %s", expected, result, one, two, input));
}
private static Stream<Arguments> provideValues() {
return Stream.of(
Arguments.of(null, null, null, true),
Arguments.of(null, 20, "foo", true),
Arguments.of(null, null, RandomStringUtils.randomAlphabetic(30), false)
);
}
}
此解决方案的局限性
供应商锁定
在这种情况下,您使用 Hibernate Validator 进行测试,这是 Bean Validation 标准的特定实现。老实说,我不认为这是一个大问题,因为 Hibernate Validator 是参考实现和最流行的 bean 验证库。但从技术上讲,这是一个供应商锁定。
跨字段验证不可用
这种解决方案仅适用于单一领域的情况。如果您有例如跨字段验证器(例如密码和确认密码匹配),则此示例不适合。
类型无关的验证需要更多的工作
就像前面提到@Size
的,注释属于基于类型(原语、集合、字符串等)的几个不同的验证器实现。使用此解决方案,您始终必须手动选择某个验证器并对其进行测试。
只能isValid
测试方法
在这种情况下,您将无法仅使用 isValid 方法测试其他内容。我的意思是例如错误消息具有预期的格式和参数或类似的东西。
总的来说,我知道创建许多具有不同注释属性的不同字段很无聊,但我非常喜欢这种方式,因为您可以测试您需要的有关验证器的所有内容。
推荐阅读
- liferay - Liferay 7.1:在我的面板应用程序中包含我的主题战争
- wordpress - Wordpress 和 PHP 5.5 - 所有图像的 WebP 转换?
- javascript - 如何仅使用 JavaScript 文件从动态创建的 img 元素中检测 onerror
- amp-html - amp-bind 中的比较运算符
- typescript - 如何将 .ts 文件的默认应用设置为 Visual Studio Code?
- python - 使用间接架构更改更新 BigQuery 视图
- c# - .net 是复制数据还是指向它
- service-worker - 从脚本标签启动时从网络下载的资产
- c# - 如何从 HostBuilder 获取 AutoFac 的 IComponentContext
- angular - Angular 4默认子路由不起作用