使用自动连线spring服务测试自定义验证器
我有一个自定义的Hibernate验证器用于我的实体。我的一个验证器使用一个自动连接的Spring@Repository。应用程序运行良好,我的存储库在我的验证器上自动连接成功 问题是我找不到一种方法来测试我的验证器,因为我无法将我的存储库注入其中 Person.class:使用自动连线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
@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();