1. 概述

URLConnection 是一个抽象类,它提供了一个接口来操作网络上的资源,比如从 URL 获取数据或向它们发送数据。

在编写单元测试时,我们通常希望有一种方法可以在不实际发起网络请求的情况下模拟网络连接和响应。

本文将探讨几种在 Java 中模拟 URLConnection 的方式。

2. 简单的 URL 获取器类

本教程中的测试焦点将是一个简单的 URL 获取器类:

public class UrlFetcher {

    private URL url;

    public UrlFetcher(URL url) throws IOException {
        this.url = url;
    }

    public boolean isUrlAvailable() throws IOException {
        return getResponseCode() == HttpURLConnection.HTTP_OK;
    }

    private int getResponseCode() throws IOException {
        HttpURLConnection con = (HttpURLConnection) this.url.openConnection();
        return con.getResponseCode();
    }
}

为了演示,我们有一个公共方法 isUrlAvailable(),它根据从 HTTP 响应消息中获取的状态码指示给定地址的 URL 是否可用。

3. 使用纯 Java 进行单元测试

通常,使用第三方测试框架处理模拟是首选。但在某些情况下,这可能不是一个可行的选择。

幸运的是,URL 类提供了一种机制,允许我们提供一个自定义处理器,该处理器知道如何建立连接。我们可以利用这个机制提供我们的处理器,它将返回一个模拟连接对象和响应。

3.1. 支持类

对于这种方法,我们需要几个支持类。首先,让我们定义一个 MockHttpURLConnection

public class MockHttpURLConnection extends HttpURLConnection {

    protected MockHttpURLConnection(URL url) {
        super(url);
    }

    @Override
    public int getResponseCode() {
        return responseCode;
    }

    public void setResponseCode(int responseCode) {
        this.responseCode = responseCode;
    }

    @Override
    public void disconnect() {
    }

    @Override
    public boolean usingProxy() {
        return false;
    }

    @Override
    public void connect() throws IOException {
    }
}

如我们所见,这个类是一个简单的扩展,对 HttpURLConnection 类进行了最小的实现。重要的是,我们提供了一个设置和获取 HTTP 响应代码的机制。

接下来,我们需要一个返回我们新创建的 MockHttpURLConnection 的模拟流处理器:

public class MockURLStreamHandler extends URLStreamHandler {

    private MockHttpURLConnection mockHttpURLConnection;

    public MockURLStreamHandler(MockHttpURLConnection mockHttpURLConnection) {
        this.mockHttpURLConnection = mockHttpURLConnection;
    }

    @Override
    protected URLConnection openConnection(URL url) throws IOException {
        return this.mockHttpURLConnection;
    }
}

最后,我们需要一个流处理器工厂,它将返回我们新创建的流处理器:

public class MockURLStreamHandlerFactory implements URLStreamHandlerFactory {

    private MockHttpURLConnection mockHttpURLConnection;

    public MockURLStreamHandlerFactory(MockHttpURLConnection mockHttpURLConnection) {
        this.mockHttpURLConnection = mockHttpURLConnection;
    }

    @Override
    public URLStreamHandler createURLStreamHandler(String protocol) {
        return new MockURLStreamHandler(this.mockHttpURLConnection);
    }
}

3.2. 整合所有内容

现在我们的支持类准备好了,我们可以开始编写第一个单元测试:

private static MockHttpURLConnection mockHttpURLConnection;

@BeforeAll
public static void setUp() {
    mockHttpURLConnection = new MockHttpURLConnection(null);
    URL.setURLStreamHandlerFactory(new MockURLStreamHandlerFactory(mockHttpURLConnection));
}

@Test
void givenMockedUrl_whenRequestSent_thenIsUrlAvailableTrue() throws Exception {
    mockHttpURLConnection.setResponseCode(HttpURLConnection.HTTP_OK);
    URL url = new URL("https://www.baeldung.com/");

    UrlFetcher fetcher = new UrlFetcher(url);
    assertTrue(fetcher.isUrlAvailable(), "Url should be available: ");
}

让我们逐步了解测试的关键部分:

  • 首先,在 setUp() 方法中,我们创建了 MockHttpURLConnection 并通过静态方法 setURLStreamHandlerFactory() 注入到 URL 类中。
  • 接下来,我们可以开始编写测试主体。首先,我们需要使用 mockHttpURLConnection 变量的 setResponseCode() 方法设置预期的响应代码。
  • 然后,我们可以创建一个新的 URL,构造我们的 UrlFetcher,最后断言 isUrlAvailable() 方法的行为。

当我们运行测试时,无论网络地址是否可用,我们都应该始终得到相同的行为。为了确保这一点,可以关闭 Wi-Fi 或网络连接,检查测试仍然以完全相同的方式运行。

3.3. 此方法的问题

尽管这个解决方案工作并且不依赖第三方库,但它有几个缺点。首先,我们需要创建多个模拟支持类,并且随着测试需求变得复杂,我们的模拟对象也会变得更加复杂。例如,如果我们需要开始模拟不同的响应体。

同样,我们的测试有一些重要的设置,其中混合了 URL 类的新实例和静态方法调用。这既令人困惑,也可能会导致未来意想不到的结果。

4. 使用 Mockito

接下来,我们将看到如何使用 Mockito,一个广为人知的单元测试框架,简化我们的测试。

首先,我们需要在项目中添加 [mockito](https://mvnrepository.com/artifact/org.mockito/mockito-core) 依赖:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

现在我们可以定义我们的测试:

@Test
void givenMockedUrl_whenRequestSent_thenIsUrlAvailableFalse() throws Exception {
    HttpURLConnection mockHttpURLConnection = mock(HttpURLConnection.class);
    when(mockHttpURLConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND);
        
    URL mockURL = mock(URL.class);
    when(mockURL.openConnection()).thenReturn(mockHttpURLConnection);
        
    UrlFetcher fetcher = new UrlFetcher(mockURL);
    assertFalse(fetcher.isUrlAvailable(), "Url should be available: ");
}

这次,我们使用 Mockito 的 mock 方法创建一个模拟的 URL 连接。然后,我们配置模拟对象,使其在 openConnection 方法被调用时返回一个模拟的 HTTP URL 连接。当然,我们的模拟 HTTP 连接已经包含了预设的响应代码。

需要注意的是,对于 Mockito 的版本低于 4.8.0,运行此测试可能会收到错误:

org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class java.net.URL
Mockito cannot mock/spy because :
 - final class

这是因为 URL 类是最终类,而在 Mockito 的早期版本中,不能直接对最终类型和方法进行模拟。

要解决这个问题,我们只需在 pom.xml 中添加额外的 依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

现在测试将成功运行!

5. 使用 JMockit

作为完整性的例子,我们将看看另一个名为 JMockit 的测试库。

首先,我们需要在项目中添加 [jmockit](https://mvnrepository.com/artifact/org.jmockit/jmockit) 依赖:

<dependency> 
    <groupId>org.jmockit</groupId> 
    <artifactId>jmockit</artifactId> 
    <version>1.49</version>
</dependency>

现在我们可以定义我们的测试类:

@ExtendWith(JMockitExtension.class)
class UrlFetcherJMockitUnitTest {

    @Test
    void givenMockedUrl_whenRequestSent_thenIsUrlAvailableTrue(@Mocked URL anyURL,
      @Mocked HttpURLConnection mockConn) throws Exception {
        new Expectations() {{
            mockConn.getResponseCode();
            result = HttpURLConnection.HTTP_OK;
        }};

        UrlFetcher fetcher = new UrlFetcher(new URL("https://www.baeldung.com/"));
        assertTrue(fetcher.isUrlAvailable(), "Url should be available: ");
    }
}

JMockit 的一个强项是其表达性。为了创建模拟并定义其行为,我们不需要调用mocking API的方法,而是直接定义它们。

6. 总结

在这篇文章中,我们学习了几种模拟 URLConnection 的方法,以便编写不依赖外部服务的独立单元测试。首先,我们使用纯 Java 示例,然后探索了使用 Mockito 和 JMockit 的其他选项。

如往常一样,文章的完整源代码可在 GitHub 上找到。