简体   繁体   中英

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?

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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM