如何Mock HttpURLConnection并测试FileNotFoundException场景
问题:Mock URLConnection触发FileNotFoundException失败
我有一个将URL渲染为PDF的Java类,核心方法如下:
public byte[] renderURLAsPDF(final String url, final Long id) throws SomeException { try { URLConnection connection = new URL(url).openConnection(); connection.setRequestProperty(HttpHeaders.USER_AGENT, ""); try (InputStream openStream = connection.getInputStream()) { PDFVerifierInputStream pdfInputStream = new PDFVerifierInputStream(openStream); return IOUtils.toByteArray(pdfInputStream); } } catch (FileNotFoundException ex) { throw SomeException.newInstanceWithHttpNotFound("Failed to find a PDF file for Booking Ref : " + itineraryItemId ); } catch (IOException e) { throw new OrionException("Failed to render URL As PDF for Booking Ref: " + itineraryItemId, e); } }
我希望编写测试用例触发FileNotFoundException对应的catch块,但当前Mock的URLConnection始终抛出IOException,无法进入预期分支。我的测试代码如下:
public void renderURLAsPDFFileNotFoundException() throws Exception { URLConnection urlConnection = mock(URLConnection.class); URLStreamHandler stubUrlHandler = new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return urlConnection; } }; String url = "https://myurl.com"; IOException fileNotFoundException = new FileNotFoundException(url); given(urlConnection.getInputStream()).willThrow(fileNotFoundException); SomeException exception = assertThrows(SomeException.class, () -> ticketPDFGateway.renderURLAsPDF(url, ITEM_ID)); assertEquals(HttpStatus.NOT_FOUND, exception.getHttpStatus()); }
问题根源
测试代码无法生效的核心原因是:生产方法内部直接调用new URL(url).openConnection()创建了全新的URL实例,并没有使用测试中定义的自定义URLStreamHandler,导致Mock的URLConnection从未被实际调用,自然不会抛出预期的FileNotFoundException。
解决方案
思路1:抽离URL构造逻辑(推荐)
通过抽离URL的创建逻辑,让生产代码具备可测试性,无需额外依赖第三方框架。
步骤1:修改生产代码
在目标类中新增一个可重写的方法用于创建URL:
// 在TicketPDFGateway类中新增 protected URL createURL(String url) throws MalformedURLException { return new URL(url); } // 修改原方法中的URL创建逻辑 public byte[] renderURLAsPDF(final String url, final Long id) throws SomeException { try { URLConnection connection = createURL(url).openConnection(); // 剩余逻辑保持不变 } catch (FileNotFoundException ex) { // ... } catch (IOException e) { // ... } }
步骤2:编写修正后的测试代码
使用Mockito的Spy重写createURL方法,返回我们自定义的URL实例:
@Test public void renderURLAsPDFFileNotFoundException() throws Exception { // 1. Mock URLConnection URLConnection urlConnection = mock(URLConnection.class); // 2. 自定义URLStreamHandler,返回mock的连接 URLStreamHandler stubUrlHandler = new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return urlConnection; } }; // 3. 使用自定义handler创建URL实例 String urlStr = "https://myurl.com"; URL mockUrl = new URL(null, urlStr, stubUrlHandler); // 4. 指定mock连接的getInputStream抛出FileNotFoundException FileNotFoundException fnfe = new FileNotFoundException(urlStr); given(urlConnection.getInputStream()).willThrow(fnfe); // 5. Spy目标类,重写createURL方法返回mockUrl TicketPDFGateway spyGateway = spy(ticketPDFGateway); doReturn(mockUrl).when(spyGateway).createURL(urlStr); // 执行测试并验证结果 SomeException exception = assertThrows(SomeException.class, () -> spyGateway.renderURLAsPDF(urlStr, ITEM_ID)); assertEquals(HttpStatus.NOT_FOUND, exception.getHttpStatus()); }
思路2:使用PowerMock拦截URL构造(适合无法修改生产代码的场景)
如果无法修改生产代码,可以使用PowerMock直接拦截URL类的构造和openConnection方法调用。
步骤1:添加PowerMock依赖
确保项目中引入PowerMock相关依赖(以Maven为例):
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency>
步骤2:编写测试代码
@RunWith(PowerMockRunner.class) @PrepareForTest({URL.class, TicketPDFGateway.class}) public class TicketPDFGatewayTest { @InjectMocks private TicketPDFGateway ticketPDFGateway; @Test public void renderURLAsPDFFileNotFoundException() throws Exception { // 1. Mock URL和URLConnection String urlStr = "https://myurl.com"; URL mockUrl = mock(URL.class); URLConnection urlConnection = mock(URLConnection.class); // 2. 拦截URL构造方法 PowerMockito.whenNew(URL.class).withArguments(urlStr).thenReturn(mockUrl); // 3. 拦截openConnection方法,返回mock的连接 PowerMockito.when(mockUrl.openConnection()).thenReturn(urlConnection); // 4. 指定mock连接抛出FileNotFoundException FileNotFoundException fnfe = new FileNotFoundException(urlStr); given(urlConnection.getInputStream()).willThrow(fnfe); // 5. 执行测试并验证 SomeException exception = assertThrows(SomeException.class, () -> ticketPDFGateway.renderURLAsPDF(urlStr, ITEM_ID)); assertEquals(HttpStatus.NOT_FOUND, exception.getHttpStatus()); } }
总结
- 思路1更符合SOLID原则,通过依赖倒置提升代码可测试性,无需额外框架,优先推荐。
- 思路2适合无法修改生产代码的场景,但需要引入PowerMock依赖,且会增加测试复杂度。
内容的提问来源于stack exchange,提问作者jitendra varshney
相关产品推荐
相关产品推荐

