2

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

gene b.
  • 10,512
  • 21
  • 115
  • 227

3 Answers3

1

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);
gene b.
  • 10,512
  • 21
  • 115
  • 227
0

Your TestPropertySource is setup incorrectly, try locations = "classpath:application-test.yaml". You also need a @SpringBootTest annotation.

The following works:

    import java.io.IOException;
    
    import org.junit.jupiter.api.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
    import org.springframework.test.context.TestPropertySource;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @SpringBootTest(webEnvironment=WebEnvironment.NONE)
    @RunWith(SpringJUnit4ClassRunner.class) 
    @TestPropertySource(locations = "classpath:application-test.yaml")
    public class AppServiceTest{
    
        @Value("${eligible_filename}")
        String eligibleFilename;

       @Value("${app.sftp.port}")
       String appSftpPort;

        
        @Test
        public void testIsEligibleFilenamePopulated() throws IOException {
            System.out.println(eligibleFilename);
            System.out.println(appSftpPort);
        }
    }

Content of src/test/resources/application-test.yaml (on the classpath):

eligible_filename: foo.bar
app:
  sftp:
    port: 1234

Output:

foo.bar
1234
John Williams
  • 4,252
  • 2
  • 9
  • 18
  • The problem is, if I put `@SpringBootTest`, that will run the entire application. I only want to run this specific Unit Test class, but with those properties retrieved. – gene b. Mar 22 '23 at 19:22
  • 1
    To populate '@Value' requires SpringContext - you can reduce the extent with 'webEnvironment=WebEnvironment.NONE. - https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/boot-features-testing.html under 47.3. I have updated my answer. – John Williams Mar 23 '23 at 08:17
  • Unfortunately `@SpringBootTest(webEnvironment = WebEnvironment.NONE)` still runs the entire Spring Boot application -- I verified it. – gene b. Mar 23 '23 at 13:49
0

Apparently @TestPropertySource and @PropertySource don't work with YAML files. This is a known issue: https://stackoverflow.com/a/61322522/1005607

I had to hack the solution with SnakeYaml,

    <dependency>
        <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId>
    </dependency>

Unit Test: Example: to get "app.sftp.port":

    Yaml yaml = new Yaml();
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("application-test.yaml");
    Map<String, Object> obj = yaml.load(inputStream);
    inputStream.close();
    
    LinkedHashMap objLevel1 = (LinkedHashMap)obj.get("app");
    LinkedHashMap objLevel2 = (LinkedHashMap)objLevel1.get("sftp");
    Integer port = (Integer)objLevel2.get("port");

It's really weird that I have to do this. You would think that at least the Yaml library would have some quick read methods for properties, but it doesn't, it just spits out a Map. Crazy!

gene b.
  • 10,512
  • 21
  • 115
  • 227