简体   繁体   中英

javax.validation.ValidationException: HV000028: Unexpected exception during isValid in Test

I can't figure out how to write a Unit Test for a Custom Constraint Validator. I have the following Validator class :

@Slf4j
@AllArgsConstructor
@NoArgsConstructor
public class SchoolIdValidator implements ConstraintValidator<ValidSchoolId, String> {

    private SchoolService schoolService;

    @Override
    public void initialize(ValidSchoolId constraintAnnotation) {}

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return schoolService.isValidSchoolId(s);
    }
}

The Validator is used by the following annotation :

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SchoolIdValidator.class)
public @interface ValidSchoolId {

    String message() default "Must be a valid school. Found: ${validatedValue}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

SchoolService is a class which calls the School Api by using ApiClient which is a Generic class with Generic restTemplate requests :

@Service
@AllArgsConstructor
public class SchoolService {

    private ApiClient apiSchoolClient;

    public Boolean isValidSchoolId(String schoolId){
        try {
            ResponseEntity res = apiSchoolClient.get(
                    String.format("/stores/%s", schoolId), Object.class, null);
            return res.getStatusCode().is2xxSuccessful();
        } catch (HttpClientErrorException e) {
            if(e.getStatusCode().value() == 404)
                return false;
            else
                throw new TechnicalException("Can't get response from school Api");
        } catch(RestClientException e) {
            throw new TechnicalException("Can't get response from School Api");
        }
    }
}

And finally the Student class :

@Data
public class Student {

    private String id;
    private String firstName;
    private String lastName;
    @ValidSchoolId
    private String schoolId;
}

When I run the app it works perfectly. But, it doesn't work at all with the following Test class :

@RunWith(MockitoJUnitRunner.class)
@ExtendWith(SpringExtension.class)
public class SchoolIdValidatorTest {

    Validator validator;

    @InjectMocks
    private SchoolService schoolService;

    @Mock
    private ApiClient apiSchoolClient;

    @Before
    public void setUp() throws Exception {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
        MockitoAnnotations.initMocks(this);
    }


    @Test
    public void isValid_SchoolIdExists_ShouldValidate() {

        Mockito.when(this.apiSchoolClient.get(Mockito.eq("/schools/12"), Mockito.any(), Mockito.any()))
                .thenReturn(new ResponseEntity<>(HttpStatus.OK));
        //given
        Student student = new Student();
        simulation.setSchoolId("12");
        //when
        Set<ConstraintViolation<Student>> violations = validator.validate(student);

        //then
        assertEquals(0, violations.size());
    }

When it comes in SchoolIdValidator schoolService is always null . So, it always throws a NPE on SchoolIdValidator.isValid. If somebody has a solution it will be really helpful.

Thx for reading and for your help.

The @InjectMocks here is likely the issue.

@RunWith(MockitoJUnitRunner.class)
@ExtendWith(SpringExtension.class)
public class SchoolIdValidatorTest {

    Validator validator;

    @InjectMocks
    private SchoolService schoolService;

along with

Validation.buildDefaultValidatorFactory().getValidator()

I'm under the assumption that SchoolService is typically a Spring managed bean, and SchoolIdValidator would have it injected. By using @InjectMocks you are saying this isn't a Spring managed bean. This class is instantiated by Mockito, so it would never be injected to your validator.

I'm not sure what the correct solution here is, since you don't post any configuration classes. Most likely, you need an additional test configuration class that instantiates your validator, ApiClient and injects your SchoolService . You can also just mock ApiClient with @MockBean which will make it a Spring managed bean, but also allow mocking.

Somewhat related, you do have constructors here. There's no reason to use @InjectMocks . Just use new SchoolService(apiSchoolClient) .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM