[英]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:47是return !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
依赖项。
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);
}
}
我知道这个问题已经很老了,但我花了一天的时间却没有找到解决问题的完整解释的真正解决方案。
所以我们开始了。 我将尽可能详细地说明事情。
在此过程结束时,您应该:
这是代码:
@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();
}
}
你需要做的是
这是自定义验证器工厂
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.