Пользовательский валидатор 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

И следующий тестовый класс:

Сотрудникнумбервалидатортест

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;
        }


    }
}

Я получаю исключение нулевого указателя в else if в классе EmployeeNumberValidator при выполнении любого из тестов:

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 и выполнение тестов для него, и не встретился с нулевым указателем. Есть ли какое-то ограничение на использование внутреннего класса с этими аннотациями, о которых я еще не читал? Я использую финальную версию 6.0.14 hibernate-validator.

Вы читали Stacktrace? Это приводит к строке, где возникает исключение, и эта строка содержит переменную empType, которая не инициализирована и в любом случае не имеет смысла.

Tom 21.01.2019 13:29

Основываясь на нескольких примерах (включая официальные документы hibernate), переменная empType не устанавливается стандартным установщиком или конструктором, значение берется из аннотации и должно быть инициализировано таким образом. До сих пор я не видел примеров, которые бы обеспечивали какой-либо механизм мутатора. Для справки: docs.jboss.org/hibernate/stable/validator/referenc‌​e/en-US/… . Из любопытства, почему, на ваш взгляд, переменная empType и последующий нулевой указатель не имеют смысла?

jbailie1991 21.01.2019 13:38

@Tom для дальнейшего использования: dolszewski.com/java/custom-parametrized-validation-annotatio‌​n , dzone.com/articles/…

jbailie1991 21.01.2019 13:44

Это не имеет смысла, потому что вам это не нужно. Вы получите значение как empNum и сможете проверить это без empType.

Tom 21.01.2019 13:52

«значение взято из аннотации и должно быть инициализировано таким образом»: no empType не будет инициализирован, а значение будет присвоено empNum. Удалите empType (или преобразуйте в локальную переменную) и работайте только с empNum (или преобразуйте вручную, преобразуйте строку из empNum в запись перечисления и назначьте ее empType)

Tom 21.01.2019 13:54

Это для примера, план состоит в том, чтобы интегрировать это в проект, над которым я сейчас работаю, поэтому будут случаи, когда эта конкретная структура и логика примеров не будут такими простыми, т.е. перечисление, которое не может быть напрямую соотнесено с проверяемым значением, поэтому в любом случае мне все еще нужно решить эту конкретную проблему для будущих приложений. Я понимаю, откуда вы взялись за этот конкретный пример, но мои варианты использования не всегда будут такими.

jbailie1991 21.01.2019 13:59

Проблема в том, что empType не будет инициализирован автоматически, поэтому, если вам это нужно, вам нужно инициализировать его вручную. Больше этого нет.

Tom 21.01.2019 14:08

Итак, как он инициализируется для других случаев, таких как тот, который я упомянул в вопросе, который действительно работает?

jbailie1991 21.01.2019 14:09

Какой из них вы имеете в виду? Это: dolszewski.com/java/custom-parametrized-validation-annotatio‌​n ? Если да, посмотрите, где он инициализирует свою переменную. У вас нет метода initialize, чтобы сделать то же самое.

Tom 21.01.2019 14:32
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
9
34
0

Другие вопросы по теме