Search code examples
javajunitjunit5test-coverage

How to fully test coverage a constructor that has a System.getenv("name") operation inside


I am using JUNIT5, have been trying to fully coverage a piece of code that involves System.getenv(""); I writed a couple classes to replicate what I am experiencing right now and so you can use them to understand me also (minimal reproducible example):

First we have the service I need to get with full coverage (ServiceToTest.class) (it has a CustomContainer object which contains methods that it needs):

@Service
public class ServiceToTest {

    private final CustomContainer customContainer;

    public ServiceToTest() {
        Object configuration = new Object();
        String envWord = System.getenv("envword");
        this.customContainer = new CustomContainer(configuration, envWord == null ? "default" : envWord);

    }

    public String getContainerName() {
        return customContainer.getContainerName();
    }

}

CustomContainer.class:

public class CustomContainer {
    @Getter
    String containerName;
    Object configuration;

    public CustomContainer(Object configuration, String containerName) {
        this.configuration = configuration;
        this.containerName = containerName;
    }

}

I have tried using ReflectionTestUtils to set the envWord variable without success... I tried this https://stackoverflow.com/a/496849/12085680, also tried using @SystemStubsExtension https://stackoverflow.com/a/64892484/12085680, and finally I also tried using Spy like in this answer https://stackoverflow.com/a/31029944/12085680

But the problem is that this variable is inside the constructor so this only executes once and I think that it happens before any of this configs I tried before can apply, here is my test class:

@ExtendWith(MockitoExtension.class)
class TestService {
    // I have to mock this becase in real project it has methods which I need mocked behavour
    private static CustomContainer mockCustomContainer = mock(CustomContainer.class);
    // The serviceToTest class in which I use ReflectionTestUtils to use the mock above
    // Here is where the constructor gets called and it happens BEFORE (debuged) the setup method
    // which is anotated with @BeforeAll
    private static ServiceToTest serviceToTest = new ServiceToTest();

    @BeforeAll
    static void setup() {
        // set the field customContainer at serviceToTest class to mockCustomContainer
        ReflectionTestUtils.setField(serviceToTest, "customContainer", mockCustomContainer);
    }

    @Test
    void testGetContainerNameNotNull() {
        assertNull(serviceToTest.getContainerName());
    }

}

I need to write a test in which serviceToTest.getContainerName is not null but the real purpose of this is to have coverage of this sentence envWord == null ? "default" : envWord so it would be a test that is capable of executing the constructor and mocking System.getenv() so that it returns not null...

Right now the coverage looks like this and I can not find a way to make it 100% Any ideas??

coverage

EDIT: So after following tgdavies suggestion, the code can be 100% covered, so this is the way:

Interface CustomContainerFactory:

public interface CustomContainerFactory {
    CustomContainer create(Object configuration, String name);
}

CustomContainerFactoryImpl:

@Service
public class CustomContainerFactoryImpl implements CustomContainerFactory {

    @Override
    public CustomContainer create(Object configuration, String name) {
        return new CustomContainer(configuration, name);
    }

}

EnvironmentAccessor Interface:

public interface EnvironmentAccessor {
    String getEnv(String name);
}

EnvironmentAccessorImpl:

@Service
public class EnvironmentAccessorImpl implements EnvironmentAccessor {

    @Override
    public String getEnv(String name) {
        return System.getenv(name);
    }

}

Class ServiceToTest after refactoring:

@Service
public class ServiceToTest {

    private final CustomContainer customContainer;
    
    public ServiceToTest(EnvironmentAccessor environmentAccessor, CustomContainerFactory customContainerFactory) {
        Object configuration = new Object();
        String envWord = environmentAccessor.getEnv("anything");
        this.customContainer = customContainerFactory.create(configuration, envWord == null ? "default" : envWord);

    }

    public String getContainerName() {
        return customContainer.getContainerName();
    }

}

Finally the test case after refactoring (here is were I think it can be improved maybe?):

@ExtendWith(MockitoExtension.class)
class TestService {

    private static CustomContainer mockCustomContainer = mock(CustomContainer.class);
    private static CustomContainerFactory customContainerFactoryMock = mock(CustomContainerFactoryImpl.class);
    private static EnvironmentAccessor environmentAccessorMock = mock(EnvironmentAccessorImpl.class);

    private static ServiceToTest serviceToTest;

    @BeforeAll
    static void setup() {
        when(environmentAccessorMock.getEnv(anyString())).thenReturn("hi");
        serviceToTest = new ServiceToTest(environmentAccessorMock, customContainerFactoryMock);
        ReflectionTestUtils.setField(serviceToTest, "customContainer", mockCustomContainer);
        when(serviceToTest.getContainerName()).thenReturn("hi");
    }

    @Test
    void testGetContainerNameNotNull() {
        assertNotNull(serviceToTest.getContainerName());
    }

    @Test
    void coverNullReturnFromGetEnv() {
        when(environmentAccessorMock.getEnv(anyString())).thenReturn(null);
        assertAll(() -> new ServiceToTest(environmentAccessorMock, customContainerFactoryMock));
    }

}

Now the coverage is 100%:

coverage100

EDIT 2:

We can improve the test class and get the same 100% coverage like so:

@ExtendWith(MockitoExtension.class)
class TestService {

    private static CustomContainer mockCustomContainer = mock(CustomContainer.class);
    private static IContainerFactory customContainerFactoryMock = mock(ContainerFactoryImpl.class);
    private static IEnvironmentAccessor environmentAccessorMock = mock(EnvironmentAccessorImpl.class);

    private static ServiceToTest serviceToTest;

    @BeforeAll
    static void setup() {
        when(environmentAccessorMock.getEnv(anyString())).thenReturn("hi");
        when(customContainerFactoryMock.create(any(), anyString())).thenReturn(mockCustomContainer);
        serviceToTest = new ServiceToTest(environmentAccessorMock, customContainerFactoryMock);

    }

    @Test
    void testGetContainerNameNotNull() {
        assertNotNull(serviceToTest.getContainerName());
    }

    @Test
    void coverNullReturnFromGetEnv() {
        when(environmentAccessorMock.getEnv(anyString())).thenReturn(null);
        assertAll(() -> new ServiceToTest(environmentAccessorMock, customContainerFactoryMock));
    }

}

Solution

  • Refactor your code to make it testable, by moving object creation and static method calls to components, which you can mock in your tests:

    interface ContainerFactory {
        CustomContainer create(Object configuration, String name);
    }
    
    interface EnvironmentAccessor {
        String getEnv(String name);
    }
    
    @Service
    public class ServiceToTest {
    
        private final CustomContainer customContainer;
    
        public ServiceToTest(ContainerFactory containerFactory, EnvironmentAccessor environmentAccessor) {
            Object configuration = new Object();
            String envWord = environmentAccessor.getEnv("envword");
            this.customContainer = containerFactory.create(configuration, envWord == null ? "default" : envWord);
        }
    
        public String getContainerName() {
            return customContainer.getContainerName();
        }
    }