使用自动连线spring服务测试自定义验证器

使用自动连线spring服务测试自定义验证器,spring,validation,testing,hibernate-validator,Spring,Validation,Testing,Hibernate Validator,我有一个自定义的Hibernate验证器用于我的实体。我的一个验证器使用一个自动连接的Spring@Repository。应用程序运行良好,我的存储库在我的验证器上自动连接成功 问题是我找不到一种方法来测试我的验证器,因为我无法将我的存储库注入其中 Person.class: @Entity @Table(schema = "dbo", name = "Person") @PersonNameMustBeUnique public class Person { @Id @Gen

我有一个自定义的Hibernate验证器用于我的实体。我的一个验证器使用一个自动连接的Spring@Repository。应用程序运行良好,我的存储库在我的验证器上自动连接成功

问题是我找不到一种方法来测试我的验证器,因为我无法将我的存储库注入其中

Person.class:

@Entity
@Table(schema = "dbo", name = "Person")
@PersonNameMustBeUnique
public class Person {

    @Id
    @GeneratedValue
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @Column()
    @NotBlank()
    private String name;

    //getters and setters
    //...
}
PersonName必须是唯一的。类

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { PersonNameMustBeUniqueValidator.class })
@Documented
public @interface PersonNameMustBeUnique{
    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends javax.validation.Payload>[] payload() default {};
}
@Target({TYPE,ANNOTATION\u TYPE})
@保留(运行时)
@约束(validatedBy={PersonNameMusteUniqueValidator.class})
@记录
public@interface人名必须是唯一的{
字符串消息()默认为“”;
类[]组()默认值{};
在@BeforeClass上上课

@BeforeClass
    public static void setUpClass() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
在测试中,您需要用模拟bean替换bean:

myValidator.initialize(null);
BeanValidatorTestUtils.replaceValidatorInContext(validator, usuarioValidoValidator, e);
完成所有魔术的班级:

public class BeanValidatorTestUtils {

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static <A extends Annotation, E> void replaceValidatorInContext(Validator validator,
                                                                            final ConstraintValidator<A, ?> validatorInstance,
                                                                                E instanceToBeValidated) {
        final Class<A> anotacaoDoValidador = (Class<A>)
                                                ((ParameterizedType) validatorInstance.getClass().getGenericInterfaces()[0])
                                                    .getActualTypeArguments()[0];

        ValidationContextBuilder valCtxBuilder = ReflectionTestUtils.<ValidationContextBuilder>invokeMethod(validator,
                                                                                                "getValidationContext");
        ValidationContext<E> validationContext = valCtxBuilder.forValidate(instanceToBeValidated);
        ConstraintValidatorManager constraintValidatorManager = validationContext.getConstraintValidatorManager();

        final ConcurrentHashMap nonSpyHashMap = new ConcurrentHashMap();
        ConcurrentHashMap spyHashMap = spy(nonSpyHashMap);
        doAnswer(new Answer<Object>() {
            @Override public Object answer(InvocationOnMock invocation) throws Throwable {
                Object key = invocation.getArguments()[0];
                Object keyAnnotation = ReflectionTestUtils.getField(key, "annotation");
                if (anotacaoDoValidador.isInstance(keyAnnotation)) {
                    return validatorInstance;
                }
                return nonSpyHashMap.get(key);
            }
        }).when(spyHashMap).get(any());

        ReflectionTestUtils.setField(constraintValidatorManager, "constraintValidatorCache", spyHashMap);
    }

}
    public class EmailAlreadyExistsValidatorTest {

        @Mock
        private EmailAlreadyExists emailAlreadyExists;

        @Mock
        private ConstraintValidatorContext constraintValidatorContext;

        @Mock
        private UserRepository repository;

        private EmailAlreadyExistsValidator validator;

        @BeforeEach
        public void beforeEach() {
            MockitoAnnotations.openMocks(this);
            validator = new EmailAlreadyExistsValidator();
            validator.initialize(emailAlreadyExists);
            ReflectionTestUtils.setField(validator, "repository", repository);
        }

        @Test
        @DisplayName("Given an user with existent email then validation must fail")
        public void isValid_existentPassword_mustFail() {

            final String existentEmail = "testuser@test.com";

            User savedUser = new User("1213443455",
                  "Test User",
                  existentEmail,
                  "12345",
                  new Date());
            Optional<User> opUser = Optional.of(savedUser);
            when(repository.findByEmail(anyString())).thenReturn(opUser);

            assertFalse(validator.isValid(existentEmail,constraintValidatorContext));

        }
    
    }
公共类beanvalidatorestutils{
@SuppressWarnings({“rawtypes”,“unchecked”})
公共静态void replaceValidatorInContext(验证器验证器,
最终ConstraintValidator验证器实例,
E InstanceTobe(已验证){
最终类anotacaoDoValidador=(类)
((ParameterizedType)validatorInstance.getClass().GetGenericInterface()[0])
.getActualTypeArguments()[0];
ValidationContextBuilder valCtxBuilder=ReflectionTestUtils.invokeMethod(验证器,
“getValidationContext”);
ValidationContext ValidationContext=valCtxBuilder.forValidate(instanceToBeValidated);
ConstraintValidatorManager ConstraintValidatorManager=validationContext.getConstraintValidatorManager();
final ConcurrentHashMap nonSpyHashMap=新ConcurrentHashMap();
ConcurrentHashMap spyHashMap=spy(非spyHashMap);
doAnswer(新答案){
@重写公共对象应答(InvocationMock调用)抛出可丢弃的{
对象键=invocation.getArguments()[0];
Object keyAnnotation=ReflectionTestUtils.getField(键,“注释”);
if(anotacaodovalidator.isInstance(keyAnnotation)){
返回验证实例;
}
返回nonSpyHashMap.get(键);
}
}).when(spyHashMap).get(any());
ReflectionTestUtils.setField(constraintValidatorManager,“constraintValidatorCache”,spyHashMap);
}
}

最近,我的自定义验证器遇到了同样的问题。我需要验证传递给控制器方法的模型(方法级验证)。验证器调用了,但依赖项(@Autowired)无法注入。我花了几天时间搜索和调试整个过程。最后,我可以让它工作。我希望我的经验为其他有相同问题的人节省一些时间。以下是我的解决方案:

有一个jsr-303自定义验证器,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD,
      ElementType.PARAMETER,
      ElementType.TYPE,
      ElementType.METHOD,
      ElementType.LOCAL_VARIABLE,
      ElementType.CONSTRUCTOR,
      ElementType.TYPE_PARAMETER,
      ElementType.TYPE_USE })
@Constraint(validatedBy = SampleValidator.class)
public @interface ValidSample {
    String message() default "Default sample validation error";

    Class<?>[] groups() default {};

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

}

public class SampleValidator implements ConstraintValidator<ValidSample, SampleModel> {

    @Autowired
    private SampleService service;


    public void initialize(ValidSample constraintAnnotation) {
    //init
    }

    public boolean isValid(SampleModel sample, ConstraintValidatorContext context) {
    service.doSomething();
    return true;
    }


}
    @ComponentScan(basePackages = { "your base packages" })
    @Configurable
    @EnableWebMvc
    class SpringTestConfig {
        @Autowired
        private WebApplicationContext wac;

    @Bean
    public Validator validator() {
    SpringConstraintValidatorFactory scvf = new SpringConstraintValidatorFactory(wac.getAutowireCapableBeanFactory());
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setConstraintValidatorFactory(scvf);
    validator.setApplicationContext(wac);
    validator.afterPropertiesSet();
    return validator;
    }

    @Bean
    public MethodValidationPostProcessor mvpp() {
    MethodValidationPostProcessor mvpp = new MethodValidationPostProcessor();
    mvpp.setValidatorFactory((ValidatorFactory) validator());
    return mvpp;
    }

    @Bean
    SampleService sampleService() {
    return Mockito.mock(SampleService.class);
    }

}

@WebAppConfiguration
@ContextConfiguration(classes = { SpringTestConfig.class, AnotherConfig.class })
public class ASampleSpringTest extends AbstractTestNGSpringContextTests {

    @Autowired
    private WebApplicationContext wac;



    private MockMvc mockMvc;


    @BeforeClass
    public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);

    mockMvc = MockMvcBuilders.webAppContextSetup(wac)
                 .build();
    }



    @Test
    public void testSomeMethodInvokingCustomValidation(){
         // test implementation
         // for example:
         mockMvc.perform(post("/url/mapped/to/controller")
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(json))
                .andExpect(status().isOk());

    }

}
class MyTestSuite {
    
    private final PersonRepository mockPersonRepository = Mockito.mock(PersonRepository.class);
    private final List<ConstraintValidator<?,?>> customConstraintValidators = 
            Collections.singletonList(new PersonNameMustBeUniqueValidator(mockPersonRepository));
    private final ValidatorFactory customValidatorFactory = 
            new CustomLocalValidatorFactoryBean(customConstraintValidators);
    private final Validator validator = customValidatorFactory.getValidator();

    @Test
    void myTestCase() {
        // mock the dependency: Mockito.when(mockPersonRepository...)
        Person p = new Person();
        //setters omitted
        Set<ConstraintViolation<?>> violations = validator.validate(p);
        //assertions on the set of constraint violations
    }

}
请注意,这里我使用的是testng,但您可以使用JUnit 4。除了使用@RunWith(SpringJUnit4ClassRunner.class)运行测试,并且不扩展AbstractTestNGSpringContextTests之外,整个配置都是相同的

现在,@ValidSample可以在自定义注释的@Target()中提到的地方使用。
注意:如果要在方法级别上使用@ValidSample注释(如验证方法参数),然后您应该将class-level annotation@Validated放在其方法使用注释的类上,例如控制器或服务类上。

Spring Boot 2允许在自定义验证器中无需任何麻烦地插入Bean。Spring框架自动检测实现
ConstraintValidator
的所有类>接口,实例化它们,并连接所有依赖项

我也有类似的问题,这就是我如何实现的

第一步界面

@Documented
@Constraint(validatedBy = UniqueFieldValidator.class)
@Target({ ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueField {

    String message() default "Duplicate Name";

    Class<?>[] groups() default {};

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

您可以在测试中将以下bean添加到Spring上下文中:

@RunWith(SpringRunner.class)
@Import(LocalValidatorFactoryBean.class)
public class PersonTest {

  @Autowired
  private Validator validator;

  {
    validator.validate(new Person());
  }

  ...
}

我们还遇到了类似的问题,@Autowiring出现故障(未初始化)在ConstraintValidator类中。我们的ConstraintValidator实现的类使用了一个应该从
应用程序.yml
文件中读取的值。下面的解决方案帮助了我们,因为它使用的是纯spring作用域。希望通过适当的SpringJunit4ClassRunner可以帮助我们

import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import org.springframework.web.context.WebApplicationContext;

@WebAppConfiguration
@ContextConfiguration(classes = {ApplicationConfig.class})
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
        "spring.someConfigValue.InApplicationYaml=Value1",
})
public class MyTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    LocalValidatorFactoryBean validator;

    @Before
    public void setup() {

        SpringConstraintValidatorFactory springConstraintValidatorFactory
                    = new SpringConstraintValidatorFactory(webApplicationContext.getAutowireCapableBeanFactory());
            validator = new LocalValidatorFactoryBean();
            validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
            validator.setApplicationContext(webApplicationContext);
            validator.afterPropertiesSet();
    }

    @Test
        public void should_have_no_violations_for_all_valid_fields() {

        Set<ConstraintViolation<PojoClassWhichHaveConstraintValidationAnnotation>> violations = validator.validate(pojoClassObjectWhichHaveConstraintValidationAnnotation);

        assertTrue(violations.isEmpty());
    }

}


@Configuration
public class ApplicationConfig {

    @Value("${spring.someConfigValue.InApplicationYaml=Value1}")
    public String configValueToBeReadFromApplicationYamlFile;

}
import org.springframework.test.context.web.WebAppConfiguration;
导入org.springframework.validation.beanvalidation.localvalidatoryFactoryBean;
导入org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
导入org.springframework.web.context.WebApplicationContext;
@WebAppConfiguration
@ContextConfiguration(类={ApplicationConfig.class})
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(属性={
“spring.someConfigValue.InApplicationYaml=Value1”,
})
公共类MyTest{
@自动连线
私有WebApplicationContext WebApplicationContext;
LocalValidatoryFactoryBean验证程序;
@以前
公共作废设置(){
SpringConstraint验证数据工厂SpringConstraint验证数据工厂
=新的SpringConstraintValidatorFactory(webApplicationContext.getAutowireCapableBeanFactory());
validator=新的LocalValidatorFactoryBean();
验证程序.setConstraintValidatorFactory(springConstraintValidatorFactory);
setApplicationContext(webApplicationContext);
验证器。AfterPropertieSet();
}
@试验
public void应\u有\u无\u冲突\u所有\u有效\u字段(){
设置冲突=validator.validate(pojoClassObjectWhichHaveConstraintValidationAnnotation);
assertTrue(inflictions.isEmpty());
}
}
@配置
公共类应用程序配置{
@值(“${spring.so
@Component
@Validated
public class PersonService {

    @Autowired
    PersionList personRepository;

    public void addPerson(@UniqueField Person person) {
        personRepository.add(person);
    }
}
@RunWith(SpringRunner.class)
@Import(LocalValidatorFactoryBean.class)
public class PersonTest {

  @Autowired
  private Validator validator;

  {
    validator.validate(new Person());
  }

  ...
}
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import org.springframework.web.context.WebApplicationContext;

@WebAppConfiguration
@ContextConfiguration(classes = {ApplicationConfig.class})
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
        "spring.someConfigValue.InApplicationYaml=Value1",
})
public class MyTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    LocalValidatorFactoryBean validator;

    @Before
    public void setup() {

        SpringConstraintValidatorFactory springConstraintValidatorFactory
                    = new SpringConstraintValidatorFactory(webApplicationContext.getAutowireCapableBeanFactory());
            validator = new LocalValidatorFactoryBean();
            validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
            validator.setApplicationContext(webApplicationContext);
            validator.afterPropertiesSet();
    }

    @Test
        public void should_have_no_violations_for_all_valid_fields() {

        Set<ConstraintViolation<PojoClassWhichHaveConstraintValidationAnnotation>> violations = validator.validate(pojoClassObjectWhichHaveConstraintValidationAnnotation);

        assertTrue(violations.isEmpty());
    }

}


@Configuration
public class ApplicationConfig {

    @Value("${spring.someConfigValue.InApplicationYaml=Value1}")
    public String configValueToBeReadFromApplicationYamlFile;

}
    @Target({ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = EmailAlreadyExistsValidator.class)
    public @interface EmailAlreadyExists {
        String message() default "Email already exists in the database";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }
    public class EmailAlreadyExistsValidator implements 
    ConstraintValidator<EmailAlreadyExists, String> {

        @Autowired
        private UserRepository repository;

        @Override
        public void initialize(EmailAlreadyExists constraintAnnotation) {}

        public boolean isValid(String email, ConstraintValidatorContext context) {
            Optional<User> opUser = repository.findByEmail(email);
            return (opUser.isEmpty());
        }
    }
    public class EmailAlreadyExistsValidatorTest {

        @Mock
        private EmailAlreadyExists emailAlreadyExists;

        @Mock
        private ConstraintValidatorContext constraintValidatorContext;

        @Mock
        private UserRepository repository;

        private EmailAlreadyExistsValidator validator;

        @BeforeEach
        public void beforeEach() {
            MockitoAnnotations.openMocks(this);
            validator = new EmailAlreadyExistsValidator();
            validator.initialize(emailAlreadyExists);
            ReflectionTestUtils.setField(validator, "repository", repository);
        }

        @Test
        @DisplayName("Given an user with existent email then validation must fail")
        public void isValid_existentPassword_mustFail() {

            final String existentEmail = "testuser@test.com";

            User savedUser = new User("1213443455",
                  "Test User",
                  existentEmail,
                  "12345",
                  new Date());
            Optional<User> opUser = Optional.of(savedUser);
            when(repository.findByEmail(anyString())).thenReturn(opUser);

            assertFalse(validator.isValid(existentEmail,constraintValidatorContext));

        }
    
    }
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 MyTestSuite {
    
    private final PersonRepository mockPersonRepository = Mockito.mock(PersonRepository.class);
    private final List<ConstraintValidator<?,?>> customConstraintValidators = 
            Collections.singletonList(new PersonNameMustBeUniqueValidator(mockPersonRepository));
    private final ValidatorFactory customValidatorFactory = 
            new CustomLocalValidatorFactoryBean(customConstraintValidators);
    private final Validator validator = customValidatorFactory.getValidator();

    @Test
    void myTestCase() {
        // mock the dependency: Mockito.when(mockPersonRepository...)
        Person p = new Person();
        //setters omitted
        Set<ConstraintViolation<?>> violations = validator.validate(p);
        //assertions on the set of constraint violations
    }

}
LocalValidatorFactoryBean localValidatorFactory = new LocalValidatorFactoryBean();
localValidatorFactory.setProviderClass(HibernateValidator.class);
localValidatorFactory.setConstraintValidatorFactory(new ConstraintValidatorFactoryImpl() {
        @Override
        public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> arg0) {
            T ret = super.getInstance(arg0);
            if (ret instanceof UniqueEmailValidator) {
                ((UniqueEmailValidator) ret).setUserService(userService);
            }
            return ret;
        }
    });
localValidatorFactory.afterPropertiesSet();