You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 16:35:06