可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a custom Hibernate Validator for my entities. One of my validators uses an Autowired Spring @Repository. The application works fine and my repository is Autowired successfully on my validator.
The problem is i can't find a way to test my validator, cause i can't inject my repository inside it.
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 {};
}
The validator:
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...
}
}
And the context file has a validator bean:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
Again, it works fine, but i don't know how to test it.
My test file is:
@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");
}
}
回答1:
On @BeforeClass:
@BeforeClass
public static void setUpClass() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
And in your test you need to replace the beans with your mocked bean:
myValidator.initialize(null);
BeanValidatorTestUtils.replaceValidatorInContext(validator, usuarioValidoValidator, e);
The class that do all the magic:
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);
}
}
回答2:
Recently I had the same problem with my custom validator. I needed to validate a model being passed to a controller's method (method level validation). The validator invoked but the dependencies (@Autowired) could not be injected. It took me some days searching and debugging the whole process. Finally, I could make it work. I hope my experience save some time for others with the same problem. Here is my solution:
Having a jsr-303 custom validator like this:
@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;
}
}
You should configure spring test like this:
@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());
}
}
Note that, here I am using testng, but you can use JUnit 4. The whole configuration would be the same except that you would run the test with @RunWith(SpringJUnit4ClassRunner.class) and do not extend the AbstractTestNGSpringContextTests.
Now, @ValidSample can be used in places mentioned in @Target() of the custom annotation.
Attention: If you are going to use the @ValidSample annotation on method level (like validating method arguments), then you should put class level annotation @Validated on the class where its method is using your annotation, for example on a controller or on a service class.
回答3:
Spring Boot 2 allows to inject Bean in custom Validator without any fuss.The Spring framework automatically detects all classes which implement the ConstraintValidator
interface, instantiate them, and wire all dependencies.
I had Similar problem , this is how i have implemented.
Step 1 Interface
@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 {};
}
Step 2 Validator
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);
}
}
Usage
@Component
@Validated
public class PersonService {
@Autowired
PersionList personRepository;
public void addPerson(@UniqueField Person person) {
personRepository.add(person);
}
}