可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
We have a URL object in one of our Java classes that we want to mock, but it's a final class so we cannot. We do not want to go a level above, and mock the InputStream because that will still leave us with untested code (we have draconian test coverage standards).
I've tried jMockIt's reflective powers but we work on Macs and there are problems with the Java agent handler that I haven't been able to resolve.
So are there any solutions that do not involve using real URLs in the junit test?
回答1:
When I have a class that can't be easily mocked because it is final (or sealed in C#), my usual route is to write a wrapper around the class and use the wrapper wherever I would use the actual class. Then I would mock out the wrapper class as necessary.
回答2:
Like Rob said, if what you want is to mock the connection returned from the URL, you can extend URLStreamHandler
. For instance, with mockito:
final URLConnection mockUrlCon = mock(URLConnection.class);
ByteArrayInputStream is = new ByteArrayInputStream(
"<myList></myList>".getBytes("UTF-8"));
doReturn(is).when(mockUrlCon).getInputStream();
//make getLastModified() return first 10, then 11
when(mockUrlCon.getLastModified()).thenReturn((Long)10L, (Long)11L);
URLStreamHandler stubUrlHandler = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return mockUrlCon;
}
};
URL url = new URL("foo", "bar", 99, "/foobar", stubUrlHandler);
doReturn(url).when(mockClassloader).getResource("pseudo-xml-path");
回答3:
I went with the following:
public static URL getMockUrl(final String filename) throws IOException {
final File file = new File("testdata/" + filename);
assertTrue("Mock HTML File " + filename + " not found", file.exists());
final URLConnection mockConnection = Mockito.mock(URLConnection.class);
given(mockConnection.getInputStream()).willReturn(
new FileInputStream(file));
final URLStreamHandler handler = new URLStreamHandler() {
@Override
protected URLConnection openConnection(final URL arg0)
throws IOException {
return mockConnection;
}
};
final URL url = new URL("http://foo.bar", "foo.bar", 80, "", handler);
return url;
}
This gives me a real URL object that contains my mock data.
回答4:
I have used a URLHandler that allows me to load a URL from the classpath. So the following
new URL("resource:///foo").openStream()
would open a file named foo from within the class path. To do this, I use a common utility library and register a handler. To use this handler, you just need to call:
com.healthmarketscience.common.util.resource.Handler.init();
and the resource URL is now available.
回答5:
If you don't want to create a wrapper :
Register a URLStreamHandlerFactory
Make the method you want public
Mock the chain
abstract public class AbstractPublicStreamHandler extends URLStreamHandler {
@Override
public URLConnection openConnection(URL url) throws IOException {
return null;
}
}
public class UrlTest {
private URLStreamHandlerFactory urlStreamHandlerFactory;
@Before
public void setUp() throws Exception {
urlStreamHandlerFactory = Mockito.mock(URLStreamHandlerFactory.class);
URL.setURLStreamHandlerFactory(urlStreamHandlerFactory);
}
@Test
public void should_return_mocked_url() throws Exception {
// GIVEN
AbstractPublicStreamHandler publicStreamHandler = Mockito.mock(AbstractPublicStreamHandler.class);
Mockito.doReturn(publicStreamHandler).when(urlStreamHandlerFactory).createURLStreamHandler(Matchers.eq("http"));
URLConnection mockedConnection = Mockito.mock(URLConnection.class);
Mockito.doReturn(mockedConnection).when(publicStreamHandler).openConnection(Matchers.any(URL.class));
Mockito.doReturn(new ByteArrayInputStream("hello".getBytes("UTF-8"))).when(mockedConnection).getInputStream();
// WHEN
URLConnection connection = new URL("http://localhost/").openConnection();
// THEN
Assertions.assertThat(new MockUtil().isMock(connection)).isTrue();
Assertions.assertThat(IOUtils.toString(connection.getInputStream(), "UTF-8")).isEqualTo("hello");
}
}
PS : I don't know how to cancel the numbered list auto-spacing after last line
回答6:
I think you can use Powermock to do this. I was able to mock URL class using PowerMock lately. Hope this helps.
/* Actual class */
import java.net.MalformedURLException;
import java.net.URL;
public class TestClass {
public URL getUrl()
throws MalformedURLException {
URL url = new URL("http://localhost/");
return url;
}
}
/* Test class */
import java.net.URL;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(value = { TestClass.class })
public class TestClassTest {
private TestClass testClass = new TestClass();
@Test
public void shouldReturnUrl()
throws Exception {
URL url = PowerMockito.mock(URL.class);
PowerMockito.whenNew(URL.class).withParameterTypes(String.class)
.withArguments(Mockito.anyString()).thenReturn(url);
URL url1 = testClass.getUrl();
Assert.assertNotNull(url1);
}
}
回答7:
I would look again at why you want to mock a final data object. Since by definition you aren't subclassing the object in your actual code, and it's not going to be the object under test, you shouldn't need to white-box test this code; just pass in whatever (real) URL objects are appropriate, and check the output.
Mock objects are useful when it's difficult to create a real object appropriate, or the real object's method are either time-consuming or depend on some stateful external resource (like a database). Neither of these apply in this case so I can't see why you can't just construct a real URL object representing the appropriate resource location.
回答8:
JMockit does indeed allow you to mock a final JRE class like java.net.URL.
It seems the Attach API in jdkDir/lib/tools.jar available in implementations of JDK 1.6 other than Sun's does not work as well. I guess this stuff is still too new/advanced, or simply didn't get the necessary attention from the other JDK vendors (Apple, IBM with the J9 JDK, Oracle with the JRockit JDK).
So, if you run into problems by having tools.jar in the classpath, try using the "-javaagent:jmockit.jar" JVM argument. It tells the JVM to directly load the java agent at startup, without using the Attach API. That should work in the Apple JDK 1.5/1.6.
回答9:
Create a URL-object pointing to the test class itself.
final URL url =
new URL("file://" + getClass().getProtectionDomain().getCodeSource().getLocation().getPath());
回答10:
Does the URL class implement an interface? If so then you could instantiate it using inversion of control or a configurable factory, rather than by direct construction, this would allow you to inject/construct a test instance at test runtime rather than the final instance you currently have.