使用自动弹簧服务测试自定义验证器

时间:2014-09-01 16:42:41

标签: spring validation testing hibernate-validator

我的实体有一个自定义的Hibernate Validator。我的一个验证器使用Autowired 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
    //...
}

PersonNameMustBeUnique.class

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

验证员:

public class PersonNameMustBeUniqueValidatorimplements ConstraintValidator<PersonNameMustBeUnique, Person> {

    @Autowired
    private PersonRepository repository;

    @Override
    public void initialize(PersonNameMustBeUnique constraintAnnotation) { }

    @Override
    public boolean isValid(Person entidade, ConstraintValidatorContext context) {
        if ( entidade == null ) {
            return true;
        }

        context.disableDefaultConstraintViolation();

        boolean isValid = nameMustBeUnique(entidade, context);

        return isValid;
    }

    private boolean nameMustBeUnique(Person entidade, ConstraintValidatorContext context) {
        //CALL REPOSITORY TO CHECK IF THE NAME IS UNIQUE 
        //ADD errors if not unique...
    }
}

上下文文件有一个验证器bean:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

同样,它工作正常,但我不知道如何测试它。

我的测试文件是:

@RunWith(MockitoJUnitRunner.class)
public class PersonTest {

    Person e;
    static Validator validator;

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

    @Test
    public void name__must_not_be_null() {
        e = new Person();
        e.setName(null);
        Set<ConstraintViolation<Person>> violations = validator.validate(e);
        assertViolacao(violations, "name", "Name must not be null");
    }

}

10 个答案:

答案 0 :(得分:3)

在@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);
    }

}

答案 1 :(得分:3)

我面临着非常类似的问题:如何编写具有自动装配的配置bean的自定义验证器的纯单元测试?

我可以通过遵循以下代码(受用户thisabhishekrvce回答启发)解决问题。

这是使用@Autowired配置bean的自定义验证器的纯单元测试,该配置bean从配置文件中读取数据(代码中未显示)。

@Import({MyValidator.class})
@ContextConfiguration(classes = MyConfiguration.class, initializers = ConfigFileApplicationContextInitializer.class)
class MyValidatorTest {

  private LocalValidatorFactoryBean validator;

  @Autowired
  private ConfigurableApplicationContext applicationContext;

  @BeforeEach
  void initialize() {
    SpringConstraintValidatorFactory springConstraintValidatorFactory
        = new SpringConstraintValidatorFactory(
        applicationContext.getAutowireCapableBeanFactory());
    validator = new LocalValidatorFactoryBean();
    validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
    validator.setApplicationContext(applicationContext);
    validator.afterPropertiesSet();
  }

  @Test
  void isValid()
  {
    Set<ConstraintViolation<MyObject>> constraintViolations = validator
        .validate(myObjectInstance);
    assertThat(constraintViolations).hasSize(1);
  }

}

答案 2 :(得分:2)

最近我的自定义验证器遇到了同样的问题。我需要验证传递给控制器​​方法的模型(方法级验证)。调用验证器但无法注入依赖项(@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());

    }

}

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

现在,@ ValidSample可用于自定义注释的@Target()中提到的位置。 注意:如果要在方法级别使用@ValidSample批注(如验证方法参数),则应将类级别注释@Validated放在其方法使用批注的类中,控制器或服务类上的示例。

答案 3 :(得分:2)

我们还面临类似的问题,即ConstrainValidator类中的@Autowiring失败(未初始化)。我们的ConstraintValidator Implemented类使用的值应该从application.yml文件中读取。下面的解决方案帮助我们,因为它使用的是纯弹簧镜。希望这可以通过适当的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;

}

答案 4 :(得分:1)

Spring Boot 2允许毫不费力地将Bean注入自定义的Validator中。Spring框架自动检测实现ConstraintValidator接口的所有类,实例化它们并连接所有依赖项。

我遇到了类似的问题,这就是我的实现方式。

步骤1界面

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

第2步验证器

public class UniqueFieldValidator implements ConstraintValidator<UniqueField, Person> {
    @Autowired
    PersionList personRepository;

    private static final Logger log = LoggerFactory.getLogger(PersonRepository.class);

    @Override
    public boolean isValid(Person object, ConstraintValidatorContext context) {

        log.info("Validating Person for Duplicate {}",object);
        return personRepository.isPresent(object);

    }

} 

用法

@Component
@Validated
public class PersonService {

    @Autowired
    PersionList personRepository;

    public void addPerson(@UniqueField Person person) {
        personRepository.add(person);
    }
}

答案 5 :(得分:1)

使用JUnit4和Mockito的解决方案:

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

    @Autowired
    private Validator validator;

    @MockBean
    private PersonRepository repository;

    @Test
    public void name_must_not_be_null() {
        // given
        when(repository.findByName(any())).thenReturn(Collection.emptyList());

        Person person = new Person();
        person.setName(null);

        // when
        Set<ConstraintViolation<Person>> violations = validator.validate(person);

        // then
        assertViolation(violations, "name", "Name must not be null");
    }
}

答案 6 :(得分:1)

您可以单独测试验证器并使用反射来注入自动装配属性。

约束注解:

    @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());
        }
    }

单元测试(ReflectionTestUtils 发挥作用):

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

        }
    
    }

答案 7 :(得分:0)

您可以在测试中将以下bean添加到您的Spring Context中:

ask_price_target_good

答案 8 :(得分:0)

可能有点晚了,但我最近遇到了同样的问题,所以我会发布我是如何解决这个问题的,因为这可以帮助其他人。

问题基本上是您通过调用 Validator 获得的 Hibernate 标准 Validation.buildDefaultValidatorFactory().getValidator() 实现对 Spring 的应用程序上下文一无所知,因此它无法在您的自定义约束验证器中注入依赖项。

在 Spring 应用程序中,ValidatorValidatorFactory 接口的实现是类 LocalValidatorFactoryBean,它可以委托给 ApplicationContext 以实例化具有依赖项的约束验证器注入。

你需要做的是

  1. 使用它们的(我认为是模拟的)依赖项实例化您的约束验证器
  2. 创建您自己的 ValidatorFactory,其中包含项目符号 1 中的所有约束验证器
  3. 从此类工厂实例化您的 Validator

这是自定义验证器工厂

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
    }

}

希望有所帮助。您可以查看我的这篇文章了解更多详情:https://codemadeclear.com/index.php/2021/01/26/how-to-mock-dependencies-when-unit-testing-custom-validators/

答案 9 :(得分:0)

我通过在我的 UnitTests 中覆盖默认的 Hibernate ConstraintValidatorFactory 来实现

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();