1

I'm currently providing coverage - testing the validation of my DTO through MockMVC request call. I recently introduced a new field in my Registration ConstraintValidator, supportedSpecializations, of which I inject the values from application.properties for easy maintenance and expandability. see code fragment below:

@Component
public class RegistrationValidator implements ConstraintValidator<Registration, String> {

    //campus.students.supportedspecializations="J2E,.NET,OracleDB,MySQL,Angular"

    @Value("${campus.students.supportedspecializations}")
    private String supportedSpecializations;

    private String specializationExceptionMessage;

    //All ExceptionMessages are maintained in a separate class
    @Override
    public void initialize(Registration constraintAnnotation) {
        exceptionMessage = constraintAnnotation.regionException().getMessage();
    }

    @Override
    public boolean isValid(RegistrationData regData, ConstraintValidatorContext context) {

        String[] specializations = supportedSpecializations.split(",");
        boolean isValidSpecialization = Arrays.stream(specializations)
                    .anyMatch(spec -> spec.equalsIgnoreCase(regData.getSpec()));
        if (!isValidSpecialization){
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(specializationExceptionMessage)
                        .addConstraintViolation();
            return false;
            }
        //additional validation logic...
        return true;
    }
}

Unit tests now fail due to the field not being injected by the defined property of the @Value annotation. I'm not sure if ReflectionTestUtils could help in my case, so any suggestions are greatly appreciated about how to inject the required values in UnitTests.


Spring version is 2.1.0 I'm currently testing with the following snippet:

@InjectMocks
private StudentController mockRestController;

@Mock
private StudentService mockStudentService;

@Mock
private ValidationExceptionTranslator mockExceptionTranslator;

@Value("${campus.students.supportedspecializations}")
private String supportedSpecializations;

private MockMvc mockMvc;

private static final String VALIDATION_SUCCESSFUL = "success";
private static final String VALIDATION_FAILED = "failed";

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
    this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();

    doReturn(
            ResponseEntity.status(HttpStatus.OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(VALIDATION_SUCCESSFUL))
    .when(mockStudentService).insertStudent(Mockito.any());

    doReturn(
            ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .header("Content-Type", "application/json")
            .body(VALIDATION_FAILED))
    .when(mockExceptionTranslator).translate(Mockito.any());
}

@Test
public void testValidation_UnsupportedSpecialization() throws Exception {

    MvcResult mvcResult = mockMvc.perform(
            post("/Students").contentType(MediaType.APPLICATION_JSON_UTF8).content(
                    "{\"registrationData\":{\"spec\":\"unsupported\"}}"))
            .andExpect(status().isBadRequest())
            .andReturn();

    assertEquals(VALIDATION_FAILED, mvcResult.getResponse().getContentAsString());

    verify(mockExceptionTranslator, times(1)).translate(Mockito.any());
    verify(mockStudentService, times(0)).insertStudent(Mockito.any());
}

I tried annotating my Test class with @RunWith(SpringRunner.class) and @SpringBootTest(classes = Application.class), but the validation test still fails due to @Value not being resolved. I might be wrong but I think the ConstraintValidator's instance is created before we reach the restController, so a MockMVC perform(...) call couldn't simply just make sure the appropriate @Value in the validator gets injected into supportedSpecializations.

MEZesUBI
  • 297
  • 7
  • 17
  • Could you post your unit test ? Just to be sure but it seems you don't load spring context so no value is injected in @Value. – Xavier Bouclet Jun 03 '19 at 15:41
  • @XavierBouclet I updated the post, please take a look – MEZesUBI Jun 04 '19 at 06:21
  • 2
    You are mocking your controller and mocking a lot of things. By default Spring doesn't do anything for processing constraint validators. You will need a proper `@WebMvcTest` or full `@SpringBootTest` test for that, else `@Value` won't get resolved. next to that Spring isn't really in control of creating instances of these validators but rather the validator implementation (hibernate in this case) and depending on the version Spirng won't even process that class. – M. Deinum Jun 04 '19 at 13:16

3 Answers3

0

Yes, use ReflectionTestUtil.

Use ReflectionTestUtil.setField to set the value of supportedSpecializationsin the setup() method (junit < 1.4) or in the @Before annotated method (junit > 1.4) in your unit test.

More Details
I recommend against using MockMVC for your unit tests; it is fine for integration tests, just not unit tests.

There is no need to start Spring for a Unit Test; you never need Spring to perform injections for your unit tests. Instead, instantiate the class you are testing and call the methods directly.

Here is a simple example:

public class TestRegistrationValidator
{
  private static final String VALUE_EXCEPTION_MESSAGE = "VALUE_EXCEPTION_MESSAGE";
    private static final String VALUE_SUPPORTED_SPECIALIZATIONS = "BLAMMY,KAPOW";

    private RegistrationValidator classToTest;

    @Mock
    private Registration mockRegistration;

    @Mock
    private RegionExceptionType mockRegionExceptionType; // use the actual type of regionExcpeption.

    @Before
    public void preTestSetup()
    {
        MockitoAnnotations.initMocks(this);

        ReflectionTestUtils.setField(classToTest, "supportedSpecializations", VALUE_SUPPORTED_SPECIALIZATIONS);

        doReturn(VALUE_EXCEPTION_MESSAGE).when(mockRegionExceptionType).getMessage();

        doReturn(mockRegionExceptionType).when(mockRegion).regionException();
    }

    @Test
    public void initialize_allGood_success()
    {
        classToTest.initialize(mockRegistration);

        ...assert some stuff.
        ...perhaps verify some stuff.
    }
}
DwB
  • 37,124
  • 11
  • 56
  • 82
  • Can you please briefly describe how to do this? I'm calling the functionality through MockMVC perform method according to Baeldung's tutorial on Custom Constraints. Is there a way to extract and mock certain constraintValidator through MockMVC / Mocked RestController? – MEZesUBI Jun 04 '19 at 06:20
0

The best option i think for you is to use constructor injection in your RegistrationValidator.class , so that you can assign mock or test values directly for testing as well when required. Example :

@Component
class ExampleClass {

    final String text

    // Use @Autowired to get @Value to work.
    @Autowired
    ExampleClass(
        // Refer to configuration property
        // app.message.text to set value for 
        // constructor argument message.
        @Value('${app.message.text}') final String text) {
        this.text = text
    }

}

This way you can set your mock values to the variables for unit testing. Yes, you are right a custom constructor is not an option here, then you could introduce a configuration class where you have these values read from yml or property and autowire that in the validator .That should work for you.

Or

You can provide the @Value properties in a separate test.yml or test.properties and specify that to be taken up while running your integrated tests. In that case you should be able to resolve these values.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ExampleApplication.class)
@TestPropertySource(locations="classpath:test.properties")
public class ExampleApplicationTests {

}

The @TestPropertySource annotation has higher precedence order and it should resolve your values.

Ananthapadmanabhan
  • 5,706
  • 6
  • 22
  • 39
  • Unfortunately neither help. I updated the question with details. The issue is, that I cannot write a custom constructor for ConstraintValidator, and the second solution wouldn't work on newer Spring-Boot apps. I use 2.1.0 – MEZesUBI Jun 04 '19 at 13:13
  • Yes, you are right, then you could introduce a configuration class where you have these values read from yml or property and autowire that in the validator .That should work for you. – Ananthapadmanabhan Jun 27 '19 at 04:56
0

Solved the issue the following way: Added the following annotations to the Test Class

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc

Then autowired the controller and mockMVC, finally annotated service and translator with Spring's @MockBean

So currently it looks something like this:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
public class StudentValidatorTest {

    @Autowired
    private StudentController mockRestController;

    @MockBean
    private StudentService mockStudentService;

    @MockBean
    private ValidationExceptionTranslator mockExceptionTranslator;

    @Autowired
    private MockMvc mockMvc;

    private static final String VALIDATION_SUCCESSFUL = "success";
    private static final String VALIDATION_FAILED = "failed";

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = MockMvcBuilders.standaloneSetup(mockRestController).build();

        doReturn(
            ResponseEntity.status(HttpStatus.OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(VALIDATION_SUCCESSFUL))
        .when(mockStudentService).insertStudent(Mockito.any());

        doReturn(
                ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .header("Content-Type", "application/json")
                .body(VALIDATION_FAILED))
        .when(mockExceptionTranslator).translate(Mockito.any());
    }

//...and tests...
MEZesUBI
  • 297
  • 7
  • 17