簡體   English   中英

Validator 類中的依賴項未在單元測試中初始化

[英]Dependency inside a Validator class not initialize in unit test

來自驗證器的 Spring 單元測試問題,其中部分問題已得到解決。

我正在嘗試對在類內部具有依賴關系的 Validator 類執行單元測試。

@NoArgsConstructor
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    @Autowired
    private AccountService accountService;

    @Override
    public void initialize(final UniqueEmail constraintAnnotation) {
    }

    @Override
    public boolean isValid(final String email, final ConstraintValidatorContext context) {
        return !this.accountService.findByEmail(email).isPresent();
    }
}

這里的堆在那里UniqueEmailValidator.java:47return !this.accountService.findByEmail(email).isPresent();

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.x.x.AccountValidatorTest.shouldDetectDuplicatedEmailAddress(AccountValidatorTest.java:95)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    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.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    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.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
Caused by: java.lang.NullPointerException
    at com.x.x.validator.UniqueEmailValidator.isValid(UniqueEmailValidator.java:47)
    at com.x.x.validator.UniqueEmailValidator.isValid(UniqueEmailValidator.java:1)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:171)
    ... 43 more

我的問題是如果驗證器在單元測試中是 init ,我如何在單元測試期間提供 accountService 的注入? 在我看來,沒有注入 accountService 或其他東西,因此是 NPE。

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountValidatorTest {

    private static Validator validator;

    @BeforeClass
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Autowired
    private AccountService accountService;

    @Test
    public void shouldDetectDuplicatedEmailAddress() {

        User user = new User(); 
        // Setters omit

        // accountRepository.save(user);

        Set<ConstraintViolation<AccountRegistrationForm>> violations = validator.validate(user);

        assertEquals(1, violations.size());
    }

}

1) 如果要為驗證器類編寫單元測試,請通過模擬所有依賴項來隔離依賴項。

你有兩種方法:

  • a) 注入UniqueEmailValidator bean,例如:

    @Autowired UniqueEmailValidator UniqueEmailValidator;

並使用模擬框架(Mockito 很好)來模擬accountService依賴項。

  • b) 使用new運算符創建UniqueEmailValidator並用 mockito Runner 替換 Spring runner。
    它將加快測試執行速度。

2) 而如果您想編寫集成測試,請注意您的測試類中使用的@DataJpaTest注釋限制 Spring 加載主要包含 JPA 組件的受限上下文。
@DataJpaTest指出:

可以與@RunWith(SpringRunner.class) 結合使用的注解,用於典型的 JPA 測試。 當測試僅關注 JPA 組件時可以使用。

並且您的服務不是 JPA 組件,因此依賴項不是由Validator bean 中的 spring 連接的。

因此,要么@Autowired服務和驗證器並將服務設置為validator要么使事情更簡單:使用@SpringBootTest而不是@DataJpaTest

在大衛的幫助下,我想我意識到我把單元測試和集成測試搞混了。 所以基本上用單元測試,下面應該足夠了,當然需要更多的測試,但這就是想法。

@RunWith(MockitoJUnitRunner.class)
public class AccountValidatorTest {

    private UniqueEmailValidator uniqueEmailValidator;

    @Mock
    private AccountService accountService;

    @Before
    public void setUp() {
        this.uniqueEmailValidator = new UniqueEmailValidator(this.accountService);
    }

    @Test
    public void shouldDetectDuplicatedEmailAddress() {

        // create user object with email "hello@world.com"
        when(accountService.findByEmail(Mockito.anyString())).thenReturn(user);

        boolean violations = uniqueEmailValidator.isValid("hello@world.com", null);

        assertFalse(violations);
    }

    @Test
    public void shouldNotDetectDuplicatedEmailAddress() {
        when(accountService.findByEmail(Mockito.anyString())).thenReturn(Optional.empty());

        boolean violations = uniqueEmailValidator.isValid("hello@world.com", null);

        assertTrue(violations);
    }
}

我知道這個問題已經很老了,但我花了一天的時間卻沒有找到解決問題的完整解釋的真正解決方案。

所以我們開始了。 我將盡可能詳細地說明事情。

在此過程結束時,您應該:

  • 注釋接口: UniqueEmail
  • 自定義約束驗證器: UniqueEmailValidator
  • 驗證器測試助手配置: ValidatorTestHelperConfig
  • 驗證器測試助手(這是為了清楚起見): ValidatorTestHelper
  • 您的測試類: AccountValidatorTest

這是代碼:

@Documented
@Target({TYPE, FIELD, ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {

    String message() default "{com.x.x.validator.UniqueEmail.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

在您的驗證器中,您不需要放置@NoArgsConstructor

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    @Autowired
    private AccountService accountService;

    @Override
    public void initialize(final UniqueEmail constraintAnnotation) { }

    @Override
    public boolean isValid(final String email, final ConstraintValidatorContext context) {
        return !this.accountService.findByEmail(email).isPresent();
     }
}

接下來,您將需要配置類

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class ValidatorTestHelperConfiguration {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

為了更干凈的測試,我們寫了一個測試助手(我只放了相關的導入)

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { ValidatorTestHelperConfiguration.class })
public abstract class ValidatorTestHelper {

    @Autowired
    protected Validator validator;

    protected List<String> getPropertyPaths(Set<? extends ConstraintViolation<?>> violations) {
        return violations.stream().map(ConstraintViolation::getPropertyPath).map(Object::toString).collect(Collectors.toList());
    }

    protected List<String> getMessageTemplate(Set<? extends ConstraintViolation<?>> violations) {
        return violations.stream().map(ConstraintViolation::getMessageTemplate).map(msg -> msg.replaceAll("([{}])", "")).collect(Collectors.toList());
    }
}

這是最后一塊,你的測試。 我正在使用 JUnit5,因此使用了 @ExtendWith(所以你知道,這行不是強制性的)。 請注意,我在這里擴展了助手。

import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class AccountValidatorTest extends ValidatorTestHelper {

    @MockBean
    private AccountService accountService;

    @Test
    public void shouldDetectDuplicatedEmailAddress() {
        User user = new User(); 
        // other things

        Optional<User> userOptional = Optional.of(mock(User.class));
        when(this.accountService.findByEmail(user.getEmail())).thenReturn(userOptional);


        Set<ConstraintViolation<AccountRegistrationForm>> violations = validator.validate(user);

        assertEquals(1, violations.size());

        assertThat(getMessageTemplate(validate)).containsOnlyElementsOf(asList(
            "{com.x.x.validator.UniqueEmail.message}")
        );

        assertThat(getPropertyPaths(validate)).containsExactlyInAnyOrder(
            "accountRegistrationForm.email"
        );
   }
}

就是這樣,希望這會有所幫助。

如果你想走純單元測試的道路,你需要一個自定義的驗證器工廠。 我會告訴你我是如何解決這個問題的。

問題基本上是您通過調用 Validation.buildDefaultValidatorFactory().getValidator() 獲得的 Hibernate 的標准 Validator 實現對 Spring 的應用程序上下文一無所知,因此它無法在您的自定義約束驗證器中注入依賴項。

在 Spring 應用程序中,Validator 和 ValidatorFactory 接口的實現都是類 LocalValidatorFactoryBean,它可以委托給 ApplicationContext 以實例化具有注入依賴項的約束驗證器。

首先要做的是用構造函數注入替換字段注入

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    private final AccountService accountService;
    
    @Autowired
    public UniqueEmailValidator(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public void initialize(final UniqueEmail constraintAnnotation) {
    }

    @Override
    public boolean isValid(final String email, final ConstraintValidatorContext context) {
        return !this.accountService.findByEmail(email).isPresent();
    }
}

你需要做的是

  1. 使用它們的(模擬的,我認為)依賴項實例化您的約束驗證器
  2. 創建您自己的 ValidatorFactory 來保存子彈點 1 中的所有約束驗證器
  3. 從這樣的工廠實例化你的驗證器

這是自定義驗證器工廠

public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {

    private final List<ConstraintValidator<?, ?>> customConstraintValidators;

    public CustomLocalValidatorFactoryBean(List<ConstraintValidator<?, ?>> customConstraintValidators) {
        this.customConstraintValidators = customConstraintValidators;
        setProviderClass(HibernateValidator.class);
        afterPropertiesSet();
    }

    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        super.postProcessConfiguration(configuration);
        ConstraintValidatorFactory defaultConstraintValidatorFactory =
                configuration.getDefaultConstraintValidatorFactory();
        configuration.constraintValidatorFactory(
                new ConstraintValidatorFactory() {
                    @Override
                    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
                        for (ConstraintValidator<?, ?> constraintValidator : customConstraintValidators) {
                            if (key.equals(constraintValidator.getClass())) //noinspection unchecked
                                return (T) constraintValidator;
                        }
                        return defaultConstraintValidatorFactory.getInstance(key);
                    }

                    @Override
                    public void releaseInstance(ConstraintValidator<?, ?> instance) {
                        defaultConstraintValidatorFactory
                                .releaseInstance(instance);
                    }
                }
        );
    }

}

然后在您的測試課程中,您只需執行以下操作:

class AccountValidatorTest {
    
    private final AccountService mockAccountService = Mockito.mock(AccountService.class);
    private final List<ConstraintValidator<?,?>> customConstraintValidators = 
            Collections.singletonList(new UniqueEmailValidator(mockAccountService));
    private final ValidatorFactory customValidatorFactory = 
            new CustomLocalValidatorFactoryBean(customConstraintValidators);
    private final Validator validator = customValidatorFactory.getValidator();
        
        @Test
        public void shouldDetectDuplicatedEmailAddress() {
        // mock the dependency: Mockito.when(mockAccountService.findByEmail...)
        User user = new User(); 

        Set<ConstraintViolation<?>> violations = validator.validate(user);

        assertEquals(1, violations.size());
    }


}

希望有幫助。 您可以查看我的這篇文章以獲取更多詳細信息: https : //codemadeclear.com/index.php/2021/01/26/how-to-mock-dependencies-when-unit-testing-custom-validators/

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM