Search code examples
javajunitcode-coveragepowermockprocessbuilder

How to mock Final classes and have code coverage


I can't manage to find a solution to my JUnit problems so I have tried to simplify this to the maximum so I hope it will be easy to understand.

Basically, I am trying to test this class :

public class PB {
    public int startProcessBuilder() {
        int status = 1;
        try {
            ProcessBuilder pb = new ProcessBuilder("java", "-jar", ".....");
            pb.directory(new File("/directory"));
            Process process = pb.start();
            status = process.waitFor();
        } catch (IOException | InterruptedException e) {
            System.out.println(e.getMessage());
        }
        return status;
    }
}

So I came up with this test :

@RunWith(PowerMockRunner.class)
@PrepareForTest({ ProcessBuilder.class, PB.class })
public class PBTest {

    private PB spyInstance = Mockito.spy(PB.class);
    private ProcessBuilder processBuilderMock = PowerMockito.mock(ProcessBuilder.class);
    private Process processMock = Mockito.mock(Process.class);

    @Before
    public void initialize() throws Exception {
        PowerMockito.whenNew(ProcessBuilder.class).withParameterTypes(String[].class).withArguments(anyVararg())
                .thenReturn(processBuilderMock);
        PowerMockito.doReturn(processMock).when(processBuilderMock).start();
    }

    @Test
    public void testStartProcessBuilder() throws Exception {
        assertThat(spyInstance.startProcessBuilder(), is(0));
    }
}

I know that my test is running successful but in the company I am working for, we are using jacoco and eclemma to display the code coverage and It is a known issue that all the code is shown as 0% coverage if the class we are testing is in the @PrepareForTest annotation.

So there is a known solution we are now using for a while, using the MockitoJUnitRunner (http://www.notonlyanecmplace.com/make-eclemma-test-coverage-work-with-powermock/)

@RunWith(MockitoJUnitRunner.class)
@PrepareForTest({ ProcessBuilder.class, PB.class })
public class PBTest {

    private PB spyInstance = Mockito.spy(PB.class);
    private ProcessBuilder processBuilderMock = PowerMockito.mock(ProcessBuilder.class);
    private Process processMock = Mockito.mock(Process.class);

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    static {
        PowerMockAgent.initializeIfNeeded();
    }

    @Before
    public void initialize() throws Exception {
        PowerMockito.whenNew(ProcessBuilder.class).withParameterTypes(String[].class).withArguments(anyVararg())
                .thenReturn(processBuilderMock);
        PowerMockito.doReturn(processMock).when(processBuilderMock).start();
    }

    @Test
    public void testStartProcessBuilder() throws Exception {
        assertThat(spyInstance.startProcessBuilder(), is(0));
    }
}

Now comes the real problems : When I try to run my test, this exception shows up : org.mockito.exceptions.misusing.NotAMockException: Argument passed to when() is not a mock! and this line is shown :

PowerMockito.doReturn(processMock).when(processBuilderMock).start();

So yeah, obviously processBuilderMock is not a mock but a powermock, so I have tried to replace these 2 lines

private ProcessBuilder processBuilderMock = PowerMockito.mock(ProcessBuilder.class);

PowerMockito.doReturn(processMock).when(processBuilderMock).start();

by this :

private ProcessBuilder processBuilderMock = Mockito.mock(ProcessBuilder.class);

PowerMockito.doReturn(processMock).when(processBuilderMock).start();

but then of course : Cannot mock/spy class java.lang.ProcessBuilder... because it is a final class (probably why I was using PowerMock in the first place)

What are my options?


Solution

  • You can design PB class to be easy to test. One way to do it would be to extract ProcessBuilder parameters:

    public class PB {
      public int startProcessBuilder(String... args) {
        try {
          ProcessBuilder pb = new ProcessBuilder(args);
    

    Later in the test you could use a small "Hello World" test JAR:

    new PB().startProcessBuilder("java", "-jar", "path-to-test-jar");
    

    or use standard echo command which should have the same syntax regardless of the OS:

    new PB().startProcessBuilder("echo", "Hello", "World");
    

    you don't need to mock anything and you actually invoke a mock Java process with a mock JAR.

    The fact that you are going for so much trouble to boost coverage highlights that your current development process is questionable. Coverage is not a goal in itself, it's a metric that should give you confidence in the code. If you have to boost it by avoiding @PrepareForTest which works well what's the point?