【问题标题】:Mocking/Testing HTTP Get Request模拟/测试 HTTP 获取请求
【发布时间】:2015-08-27 18:54:20
【问题描述】:

我正在尝试为我的程序编写单元测试并使用模拟数据。我对如何拦截对 URL 的 HTTP Get 请求有点困惑。

我的程序调用我们 API 的 URL,并返回一个简单的 XML 文件。我希望测试不是从在线 API 获取 XML 文件,而是从我这里接收预定的 XML 文件,以便我可以将输出与预期输出进行比较,并确定一切是否正常。

我被指向 Mockito 并且已经看到了许多不同的示例,例如这个 SO 帖子,How to use mockito for testing a REST service?,但我不清楚如何设置它以及如何模拟数据(即,返回我自己的 xml每当调用 URL 时文件)。

我唯一能想到的是让另一个程序在 Tomcat 上本地运行,并在我的测试中传递一个特殊的 URL,该 URL 调用 Tomcat 上本地运行的程序,然后返回我想要测试的 xml 文件。但这似乎有点矫枉过正,我认为这是不可接受的。有人能指出我正确的方向吗?

private static InputStream getContent(String uri) {

    HttpURLConnection connection = null;

    try {
        URL url = new URL(uri);
        connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Accept", "application/xml");

        return connection.getInputStream();
    } catch (MalformedURLException e) {
        LOGGER.error("internal error", e);
    } catch (IOException e) {
        LOGGER.error("internal error", e);
    } finally {
        if (connection != null) {
            connection.disconnect();
        }
    }

    return null;
}

如果有帮助,我正在使用 Spring Boot 和 Spring 框架的其他部分。

【问题讨论】:

  • 你能贴出你想测试的代码单元吗?
  • 对不起,我本来打算这样做的。我不知道我是否也应该发布辅助功能?主要部分我想看看如何用假数据模拟那个 URL 调用。
  • 你总是可以使用在 localhost 上运行的网络服务器而不是 mockito mock。
  • 我如何将其封装到一个项目中,以便其他开发人员可以在未来几年内复制我的测试?它总是两个独立的应用程序,不是吗?
  • 您可以在单元测试中轻松启动 HTTP 服务器。看看undertow.io

标签: java unit-testing http junit mockito


【解决方案1】:

部分问题在于您没有将事物分解为接口。您需要将getContent 包装到一个接口中,并提供一个实现该接口的具体类。然后这个具体的类 需要传递到使用原始getContent 的任何类中。 (这本质上是依赖倒置。)您的代码最终会看起来像这样。

public interface IUrlStreamSource {
    InputStream getContent(String uri)
}

public class SimpleUrlStreamSource implements IUrlStreamSource {
    protected final Logger LOGGER;

    public SimpleUrlStreamSource(Logger LOGGER) {
      this.LOGGER = LOGGER;
    }

    // pulled out to allow test classes to provide
    // a version that returns mock objects
    protected URL stringToUrl(String uri) throws MalformedURLException {
        return new URL(uri);
    }

    public InputStream getContent(String uri) {

        HttpURLConnection connection = null;

        try {
            Url url = stringToUrl(uri);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Accept", "application/xml");

            return connection.getInputStream();
        } catch (MalformedURLException e) {
            LOGGER.error("internal error", e);
        } catch (IOException e) {
            LOGGER.error("internal error", e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }

        return null;
    }
}

现在使用静态getContent 的代码应该通过IUrlStreamSource 实例getContent()。然后,您向要测试模拟的对象提供 IUrlStreamSource 而不是 SimpleUrlStreamSource

如果您想测试SimpleUrlStreamSource(但没有太多要测试的东西),那么您可以创建一个派生类,该派生类提供stringToUrl 的实现,该实现返回一个模拟(或引发异常)。

【讨论】:

  • 这是完美的。这实际上是我应该如何编写我的代码......谢谢。我不知道为什么我没有想到这一点。
  • 这种方法激励程序员修改他们的代码,以便测试可以注入或覆盖行为。请记住,这也会修改您导出的 API(在本例中为受保护的方法)。我会尽量避免修改我的 API 以使其“可测试”。您可能会意外暴露程序的某些部分。
【解决方案2】:

此处的其他答案建议您将代码重构为使用一种可以在测试期间替换的提供程序 - 这是更好的方法。

如果由于某种原因无法实现,您可以安装自定义 URLStreamHandlerFactory,它会拦截您想要“模拟”的 URL,并回退到不应被拦截的 URL 的标准实现。

请注意,这是不可逆的,因此您无法在安装后删除 InterceptingUrlStreamHandlerFactory - 摆脱它的唯一方法是重新启动 JVM。您可以在其中实现一个标志来禁用它并为所有查找返回null - 这将产生相同的结果。

URLInterceptionDemo.java:

public class URLInterceptionDemo {

    private static final String INTERCEPT_HOST = "dummy-host.com";

    public static void main(String[] args) throws IOException {
        // Install our own stream handler factory
        URL.setURLStreamHandlerFactory(new InterceptingUrlStreamHandlerFactory());

        // Fetch an intercepted URL
        printUrlContents(new URL("http://dummy-host.com/message.txt"));
        // Fetch another URL that shouldn't be intercepted
        printUrlContents(new URL("http://httpbin.org/user-agent"));
    }

    private static void printUrlContents(URL url) throws IOException {
        try(InputStream stream = url.openStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
            String line;
            while((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }

    private static class InterceptingUrlStreamHandlerFactory implements URLStreamHandlerFactory {

        @Override
        public URLStreamHandler createURLStreamHandler(final String protocol) {
            if("http".equalsIgnoreCase(protocol)) {
                // Intercept HTTP requests
                return new InterceptingHttpUrlStreamHandler();
            }
            return null;
        }
    }

    private static class InterceptingHttpUrlStreamHandler extends URLStreamHandler {

        @Override
        protected URLConnection openConnection(final URL u) throws IOException {
            if(INTERCEPT_HOST.equals(u.getHost())) {
                // This URL should be intercepted, return the file from the classpath
                return URLInterceptionDemo.class.getResource(u.getHost() + "/" + u.getPath()).openConnection();
            }
            // Fall back to the default handler, by passing the default handler here we won't end up
            // in the factory again - which would trigger infinite recursion
            return new URL(null, u.toString(), new sun.net.www.protocol.http.Handler()).openConnection();
        }
    }
}

dummy-host.com/message.txt

Hello World!

运行时,这个应用会输出:

Hello World!
{
  "user-agent": "Java/1.8.0_45"
}

更改决定拦截哪些 URL 以及返回什么的标准非常容易。

【讨论】:

    【解决方案3】:

    答案取决于您要测试的内容。

    如果需要测试InputStream的处理

    如果getContent() 被一些处理InputStream 返回的数据的代码调用,并且您想测试处理代码如何处理特定的输入集,那么您需要创建一个接缝来启用测试。我只需将getContent() 移动到一个新类中,然后将该类注入到进行处理的类中:

    public interface ContentSource {
    
      InputStream getContent(String uri);
    }
    

    您可以创建一个使用URL.openConnection()HttpContentSource(或者更好的是,Apache HttpClientcode)。

    然后您将ContentSource 注入处理器:

    public class Processor {
      private final ContentSource contentSource;
    
      @Inject
      public Processor(ContentSource contentSource) {
        this.contentSource = contentSource;
      }
    
      ...
    }
    

    Processor 中的代码可以使用模拟 ContentSource 进行测试。

    如果您需要测试内容的提取

    如果您想确保 getContent() 正常工作,您可以创建一个测试,启动一个轻量级内存中 HTTP 服务器来提供预期的内容,并让 getContent() 与该服务器通信。这似乎有点矫枉过正。

    如果您需要使用假数据测试系统的大部分子集

    如果您想确保端到端工作,请编写端到端系统测试。由于您表示您使用 Spring,因此您可以使用 Spring 将系统的各个部分连接在一起(或连接整个系统,但具有不同的属性)。你有两个选择

    1. 让系统测试启动本地 HTTP 服务器,并在测试创建系统后,将其配置为与该服务器通信。有关启动 HTTP 服务器的方法,请参阅this question 的答案。

    2. 配置 spring 以使用 ContentSource 的假实现。这会让你对所有东西都是端到端工作的信心稍微降低一些,但它会更快,更不容易出错。

    【讨论】:

    • 最终我正在测试程序的其余部分。为了测试程序的其余部分,我想给它一个预定义的 xml 文件作为输入,并检查程序的输出以确保它处理一切正常。为此,我将问题缩小到这个 getContent() 函数。如果我能找到一种方法来拦截该 HTTP GET 请求并注入我自己的 xml 文件,那么我将能够确定最终结果。我不知道如何创建端到端测试或设置内存服务器。你能给我举个例子吗?如果有帮助,我正在使用 Spring Boot、Maven 和 Netbeans。谢谢
    • 如果您使用的是 spring,那么您将创建一个将您的大部分对象连接在一起的配置,但将一些对象替换为您控制的假版本。
    • 至于如何设置内存服务器,有很多可能性。我喜欢 Python SimpleHTTPServer 类。如果您需要 Java 解决方案,请参阅stackoverflow.com/q/3732109/95725 的答案
    猜你喜欢
    • 1970-01-01
    • 2014-04-23
    • 1970-01-01
    • 1970-01-01
    • 2021-04-10
    • 2019-08-19
    • 1970-01-01
    • 2016-11-19
    • 1970-01-01
    相关资源
    最近更新 更多