Search code examples
springspring-bootunit-testingjunit

@Value variables are NULL in Unit Test with @TestPropertySource


In the following unit test, where I provide properties both manually and try to read them from an existing YAML resource file (different strategies were tried with @TestPropertySource), the @Value{..} properties don't get set and I'm always getting NULL for them:

@SpringBootConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = {
        "eligible_filename = xyz"
})
@ExtendWith(MockitoExtension.class)
public class AppServiceTest {
    
    @Value("${eligible_filename}") // This var is always NULL

as well as

@SpringBootConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(properties = "application-test.yaml") /* File exists, also tried resources/application-test.yaml */
@ExtendWith(MockitoExtension.class)
public class AppServiceTest {
    
    @Value("${eligible_filename}") // This var is always NULL

enter image description here


Solution

  • Here's what I've finally settled on. We must use @SpringBootTest to load in the test properties (application-test.yaml) and get the @Value(..), but that unfortunately causes the whole application to run. To block out the real objects being used, and substitute mock objects instead, we can use @SpringBootTest with @Import(SomeConfiguration.class) as follows. This will make the mock object get picked up on running the @SpringBootTest test:

    @SpringBootTest
    @Import(value = {MockAppServiceConfiguration.class, 
                     MockEmailServiceConfiguration.class}) // etc. any other mocked objects
    public class MyTest {
    }
    

    example of a Mock Service Configuration:

    public class MockEmailServiceConfiguration {
        
        // Replace with mock EmailService when running full-application @SpringBootTest tests
        @Bean
        public EmailService emailService(){
            return new EmailService() {
               //... Override any service methods with a mock result (no real processing)
            }
    

    Now you can run the test without getting real objects wired. But since we still need to unit-test the actual service class, I manually create my own Service object and do Mockito's testing on it with openMocks / @Mock / @InjectMocks). Here's Part 2 for testing the actual Service class:

    @SpringBootTest
    @Import(value = {MockAr11ApplicationServiceConfiguration.class, /* Prevent the loading of the real Ar11ApplicationService during @SpringBootTest's full-application launch */
                     MockEmailServiceConfiguration.class}) /* Prevent the loading of the real EmailService during @SpringBootTest's full-application launch */
    @ExtendWith(MockitoExtension.class)
    public class Ar11ApplicationServiceTest {
        
        // Custom object to be constructed for unit testing, includes DAO sub-object via InjectMocks
        @InjectMocks
        Ar11ApplicationServiceImpl ar11ApplicationServiceImpl;
        // Custom DAO sub-object to be constructed for this unit test via Mock
        @Mock
        private Ar11ApplicationDAO ar11ApplicationDAO;
    
        @BeforeEach
        void setUp() throws Exception {
            // Manually construct/wire my own custom objects for unit testing
            ar11ApplicationServiceImpl = new Ar11ApplicationServiceImpl();
            ar11ApplicationDAO = new Ar11ApplicationDAO();
            ar11ApplicationServiceImpl.setAr11ApplicationDAO(ar11ApplicationDAO);
            MockitoAnnotations.openMocks(this);
    
            // Set up mock behaviors for the DAO sub-object with when/then
            when(ar11ApplicationDAO.method(filename)).thenReturn(..)
            doNothing().when(ar11ApplicationDAO).voidMethod(params);