The Dark Powers of PowerMock

Recently, we’ve started using Mockito and PowerMock in our testing. I won’t explain mocking and why or why not you should use it, but I want to share my experience with using PowerMock.

PowerMock comes with a very strong promise: “PowerMock uses a custom classloader and bytecode manipulation to enable mocking of static methods, constructors, final classes and methods, private methods, removal of static initializers and more.”

That is seriously cool, right? I thought so, too, but I stumbled upon several problems the very first time I tried to use it. Frankly, those problems, as always, stemmed from my lack of experience with the tool, but hey – everyone’s a novice at first. Let me share my experience with you.

The Problem

 
public class ClassUnderTest { 
    public InputStream method(boolean param, URI uri) throws Exception { 
        String scheme = param ? "https" : "http"; 
        URI replacedUri = new URI(scheme, uri.getAuthority(), uri.getPath(), uri.getQuery(), uri.getFragment()); 
        return replacedUri.toURL().openStream(); 
    } 
}

The above fabricated example expresses the essence of the testing challenge I faced (the real class was this.) The method I wanted to test obtains an URI and transforms it based on some parameters. Then it tries to open a stream on the URI so that the caller can download the contents.

Because the URI that the method tries to download from is by design either http or https URL, it is kind of hard to test without actually standing up a HTTP server to serve the file during the test. This is of course not impossible and possibly would not be that hard, but I thought PowerMock can come here to the rescue. I should be able to mock those calls out in my tests.

Attempt #1 – mocking system classes

 
@Test
@PrepareForTest(ClassUnderTest.class)
public class MyTest { 
    @ObjectFactory 
    public IObjectFactory getObjectFactory() { 
        return new PowerMockObjectFactory(); 
    } 

    public void testMethod() throws Exception { 
        URI uriMock = PowerMockito.mock(URI.class); 
        URL urlMock = PowerMockito.mock(URL.class); 
        
        PowerMockito.whenNew(URI.class).withArguments("http", "localhost", null, null, null).thenReturn(uriMock); 
        Mockito.when(uriMock.toURL()).thenReturn(urlMock); 
        Mockito.when(urlMock.openStream()).thenReturn(new FileInputStream(new File(".", "existing.file"))); 
        
        ClassUnderTest testObject = new ClassUnderTest(); 
        testObject.method(false, new URI("blah://localhost")); 
    } 
} 

This should be fairly easy to understand for everyone that used some mocking framework. I’m creating two mocks: one for URI and one for URL classes. Then I’m using PowerMock to capture the construction of a new URI (see the code of the ClassUnderTest) and returning my uriMock. The uriMock is set up to return the urlMock when its toURL() method is called. When the openStream() method is called on my urlMock, I’m returning an input stream of a local file.

Nice and easy, right? Except it doesn’t work. I get the following stacktrace as soon as I try to mock the URI class:

 org.mockito.exceptions.base.MockitoException: Mockito cannot mock this class: class replica.java.net.URI$$PowerMock0 Mockito can only mock visible & non-final classes. 

After a bit of googling, the cause is apparent – PowerMock cannot mock the system classes (unless PowerMock java agent is used). Ok, let’s try another approach, this time trying to avoid using mocks.

Attempt #2 – PowerMockito.whenNew(URL.class)

The idea behind this attempt is that PowerMockito can capture and override constructor calls. Because URI.toURL() constructs a new URL instance with a single string argument, so we theoretically should be able to intercept that?

 
public void testMethod() throws Exception { 
    URL realUrl = new File(".", "existing.file").toURI().toURL(); 

    PowerMockito.whenNew(URL.class).withArguments("http://localhost").thenReturn(realUrl); 
    
    ClassUnderTest testObject = new ClassUnderTest(); 
    testObject.method(false, new URI("blah://localhost")); 
} 

As you might have guessed, this doesn’t work either. And frankly if it did, I’d have some serious questions about how it could. The constructor of URL is only called inside the toURL() of the URI which is a system class that PowerMock can’t touch. So, the third attempt.

Attempt #3 – PowerMockito.whenNew(URI.class)

What is the difference between this one and the previous attempt? Well, it took me a while to decipher the javadoc for the @PrepareForTest annotation, but it boils down to this. If you need to use the PowerMockito.whenNew method, you need to tell PowerMock to do bytecode manipulation on the class that (in some method) directly calls given constructor. This is kinda understandable when you know what PowerMock is doing – it will actually change the byte code of the “prepared” class so that any constructor calls (and other things) are checking for the rules defined using whenNew and other methods. You realize this for real when you try to debug the class under test (that has been prepared by power mock) – you can no longer be sure that what you see in the code is actually what is happening, because the bytecode of the class no longer exactly corresponds to what you see in the source code.

So to sum it up, here’s the code that works:

 
@Test 
@PrepareForTest(ClassUnderTest.class) 
public class MyTest { 
    @ObjectFactory 
    public IObjectFactory getObjectFactory() { 
        return new PowerMockObjectFactory(); 
    } 

    public void testMethod() throws Exception { 
        URI realUri = new File(".", "existing.file").toURI(); 
        PowerMockito.whenNew(URI.class).withArguments("http", "localhost", null, null, null).thenReturn(realUri); 
        
        ClassUnderTest testObject = new ClassUnderTest();         
        testObject.method(false, new URI("blah://localhost")); 
    } 
} 

The constructor of the URI is intercepted and we return a “realUri”, i.e. a different instance of otherwise “normal” URI class. This works, because exactly that constructor with those arguments is called in the class under test that has been manipulated by PowerMock (as instructed by the @PrepareForTest annotation). From that point on, we don’t need any special behavior on either the URI or URL classes and so the code can stay untouched.

Conclusion

The conclusion is basically the famous 4 letters – RTFM :-) I just wanted to detail my journey through the dark corners of the PowerMock forest just in case some of you were as confused as I was when I first entered it.

About these ads
Posted in Java, RHQ. 2 Comments »

2 Responses to “The Dark Powers of PowerMock”

  1. Yujun Liang Says:

    Actually I am not sure what you wrote is unit test, here is the test I wrote,

    package test;

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;

    import java.io.InputStream;
    import java.net.URI;
    import java.net.URL;

    import static org.easymock.EasyMock.expect;
    import static org.powermock.api.easymock.PowerMock.*;

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({ClassUnderTest.class, InputStream.class, URI.class, URL.class})
    public class ClassUnderTestTest {

    @Test
    public void testMethod() throws Exception {
    URI uri = createMock(URI.class);
    expect(uri.getAuthority()).andReturn(“X”);
    expect(uri.getPath()).andReturn(“XX”);
    expect(uri.getQuery()).andReturn(“XXX”);
    expect(uri.getFragment()).andReturn(“XXXX”);

    expectNew(URI.class,”https”, “X”,”XX”, “XXX”, “XXXX”).andReturn(uri);
    URL url = createMock(URL.class);
    expect(uri.toURL()).andReturn(url);
    expect(url.openStream()).andReturn(createMock(InputStream.class));
    replayAll();

    new ClassUnderTest().method(true, uri);
    verifyAll();
    }
    }

    If you add PwerMock.verifyAll() to your test, will it still pass?

  2. Annie Says:

    Thanks for this. You helped me find my issue, which turned out to be wrong class in @PrepareForTest..


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: