0

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.

Pierre Jones
  • 620
  • 1
  • 11
  • 24

1 Answers1

1

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

Christopher Schneider
  • 3,745
  • 2
  • 24
  • 38
  • Thanks for you reply. I tried to annotate `apiSchoolClient ` with `@Mock `and `schoolService` with `@MockBean `but it is still `null` – Pierre Jones Feb 14 '20 at 16:25
  • 1
    You wouldn't need to mock `schoolService`, just `apiSchoolClient`. If you're still seeing the error with `schoolService` as a mock bean, however, then your validator is not a Spring managed bean. You likely need to use `SpringConstraintValidatorFactory`. See https://stackoverflow.com/a/37975083/3059385 – Christopher Schneider Feb 14 '20 at 16:34
  • Most likely the correct path to follow, I'm not seeing any configuration that results in `schoolService` in `SchoolIdValidator` being anything other than `null`. – filpa Feb 15 '20 at 02:23