在Java中Mock URL连接

2025/03/30

1. 概述

UrlConnection是一个抽象类,它提供了一个处理Web资源的接口,例如从URL检索数据并向其发送数据。

在编写单元测试时,我们通常需要一种方法来模拟网络连接和响应,而无需实际发出实际的网络请求

在本教程中,我们将介绍在Java中Mock URL连接的几种方法。

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(),它指示给定地址的URL是否可用。返回值基于我们收到的HTTP响应消息的状态码。

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

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

幸运的是,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响应码的机制。

接下来,我们需要一个Mock流处理程序来返回我们新创建的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.tuyucheng.com/");

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

让我们来看看测试的关键部分:

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

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

3.3 这种方法的问题

虽然此解决方案有效并且不依赖于第三方库,但由于多种原因,它有点麻烦。

首先,我们需要创建几个Mock支持类,随着测试需求变得越来越复杂,我们的Mock对象也会变得越来越复杂。例如,如果我们需要开始Mock不同的响应主体。

同样,我们的测试有一些重要的设置,我们将静态方法调用与URL类的新实例混合在一起。这很容易让人困惑,并且可能会导致后续出现意想不到的结果。

4. 使用Mockito

在下一部分中,我们将看到如何使用著名的单元测试框架Mockito简化测试。

首先,我们需要将mockito依赖添加到项目中:

<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连接。然后,我们将Mock对象配置为在调用其openConnection方法时返回模拟HTTP URL连接。当然,我们的模拟HTTP连接已经包含存根响应代码。

我们应该注意,对于低于4.8.0的Mockito版本,我们在运行此测试时可能会收到错误:

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

发生这种情况是因为URL是一个最final类,而在以前的Mockito版本中,无法直接Mock final类和方法

为了解决这个问题,我们可以简单地在pom.xml中添加一个额外的依赖

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

现在我们的测试将成功运行。

5. 使用JMockit

在我们的最后一个例子中,为了完整性,我们将研究另一个名为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() ;

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

JMockit最强大的一点就是它的可表达性,为了创建Mock并定义其行为,我们只需直接定义它们,而不是从模拟API调用方法

6. 总结

在本文中,我们了解了几种Mock URL连接的方法,以便编写不依赖任何外部服务的独立单元测试。首先,我们查看了一个使用原生Java的示例,然后探索了使用Mockito和JMockit的另外两个选项。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章