1
2
3
4
5
6
7
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 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
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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?
1
2
3
4
5
6
7
8
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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.