首页 > 解决方案 > 自定义 bean 验证器在单元测试中抛出空指针

问题描述

我创建了一个示例自定义约束来探索休眠 bean 验证实现。约束本身相当简单;给定一个特定的字符串和一个枚举,验证器使用正则表达式来检查字符串是否匹配特定的模式(通过 EmpType 枚举选择)。我有以下内容:

员工编号.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//implementation of ConstraintValidator interface, i.e. the class that performs custom logic to validate the value
@Constraint(validatedBy = { EmployeeNumberValidator.class})
public @interface EmployeeNumber {

    //Validation message for failures
    String message() default "{EmployeeNumber.standard}";
    //allows "grouping"; can choose under what circumstances this will fire via interfaces
    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    EmpType value() default EmpType.STANDARD;
}

EmpType.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

public enum EmpType {
    ADMIN,STANDARD;
}

EmployeeNumberValidator.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.lang3.StringUtils;

/**
 * Backing validator class for EmployeeNumber bean validation annotation
 */
public class EmployeeNumberValidator implements ConstraintValidator<EmployeeNumber, String> {

    private static Pattern STANDARD_PATTERN = Pattern.compile("E\\d{6}");
    private static Pattern ADMIN_PATTERN = Pattern.compile("A\\d{6}");

    protected EmpType empType;

    /**
     * checks if emp number is not blank, and matches either standard or admin employee number format based on empType
     * 
     * @param empNum String to check for validity
     * @param constraintValidatorContext context for validation annotation
     * 
     * @return true if empNum matches empType specific regex, false if empNum is blank or does not match any regex
     */
    @Override
    public boolean isValid(final String empNum, final ConstraintValidatorContext constraintValidatorContext) {
        boolean isValid;

        if(StringUtils.isBlank(empNum)) {
            isValid = false;
        } else if(empType.equals(EmpType.STANDARD)) {
            isValid = isStandardNumberValid(empNum);
        } else {
            isValid = isAdminNumberValid(empNum);

            if(!isValid) {
                constraintValidatorContext.disableDefaultConstraintViolation();;
                constraintValidatorContext
                        .buildConstraintViolationWithTemplate("{EmployeeNumber.admin}")
                        .addConstraintViolation();
            }
        }
        return isValid;
    }

    /**
     * Compares empNum against Standard employee number pattern regex
     * @param empNum string to compare against standard pattern regex
     * 
     * @return true if match, false otherwise
     */
    private boolean isStandardNumberValid(final String empNum) {
        final Matcher matcher = STANDARD_PATTERN.matcher(empNum);

        return matcher.matches();
    }

    /**
     * Compares empNum against Admin employee number pattern regex
     * @param empNum string to compare against admin pattern regex
     *
     * @return true if match, false otherwise
     */
    private boolean isAdminNumberValid(final String empNum) {
        final Matcher matcher = ADMIN_PATTERN.matcher(empNum);

        return matcher.matches();
    }
}

使用以下自定义消息:

ValidationMessages.properties

EmployeeNumber.standard=Standard employee Number must start with E, followed by 6 digits
EmployeeNumber.admin=Admin employee number must start with A, followed by 6 digits

以及以下测试类:

EmployeeNumberValidatorTest

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import static com.lmig.beanvalidation.testutils.ValidatorUtils.getErrorMessagesFromSet;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.junit.Test;

public class EmployeeNumberValidatorTest {

    private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    private final Validator validator = factory.getValidator();

    @Test
    public void test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
        EmpNumTestClass input = new EmpNumTestClass("E123456", "A123456");
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validate(input);


        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).isEmpty();
    }

    @Test
    public void test_standardEmployeeNumber_employeeNumberIsInvalid_standardEmployeeNumberValidationFailure() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
                "standardEmpNum", "invalid");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).hasSize(1).contains("Standard employee Number must start with E, followed by 6 digits");
    }

    @Test
    public void test_adminEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class, 
                "adminEmpNum", "A123456");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).isEmpty();
    }

    @Test
    public void test_adminEmployeeNumber_employeeNumberIsInvalid_adminEmployeeNumberValidationFailure() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
                "adminEmpNum", "invalid");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).hasSize(1).contains("Admin employee number must start with A, followed by 6 digits");
    }

    class EmpNumTestClass {

        @EmployeeNumber(value = EmpType.STANDARD)
        private String standardEmpNum;
        @EmployeeNumber(value = EmpType.ADMIN)
        private String adminEmpNum;

        EmpNumTestClass(final String standardEmpNum, final String adminEmpNum) {
            this.standardEmpNum = standardEmpNum;
            this.adminEmpNum= adminEmpNum;
        }


    }
}

当运行任何测试时,如果在 EmployeeNumberValidator 类中的 else if 上出现空指针异常:

javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.

    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:177)
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:68)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:380)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:169)
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidatorTest.test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned(EmployeeNumberValidatorTest.java:24)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.NullPointerException
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:35)
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:14)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:171)
    ... 33 more

我已经在域对象上测试了这个的变体。测试布局与上面类似,但不是在测试中使用内部类,而是使用 src 文件夹树中的一个类,并且运行没有问题。除了使用内部类之外,我找不到测试之间的任何区别(作为尝试测试 EmpType 验证变体的一部分,而不会以不会在生产场景中使用的方式污染或以其他方式操纵 src 域类) . 我已经尝试了几种方法(在测试文件中看到;大多数使用 validateValue,我已经使用填充对象测试了 validateProperty 和标准验证函数)但是对于我使用的所有变体都收到相同的错误。

提到的错误意味着没有设置empType,但我不确定为什么会出现这种情况,因为我已经传入了测试内部类中存在的两个注释的值,并确认我已经尝试传入 EmpType src 类上的值并为其运行测试,并且没有遇到空指针。使用带有这些我还没有读过的注释的内部类是否存在某种约束?我正在使用 hibernate-validator 版本 6.0.14 final

标签: javajava-8bean-validationhibernate-validator

解决方案


推荐阅读