模拟/测试HTTP Get请求

6

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

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

有人向我指出了Mockito,并给出了很多不同的示例,例如stackoverflow上的这篇文章:如何使用Mockito测试REST服务?,但我仍不清楚如何设置所有内容以及如何模拟数据(即每当调用URL时返回我的xml文件)。

我唯一能想到的办法是创建另一个在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 Framework的其他部分。


你能发布你想要测试的代码单元吗? - Zac Lozano
抱歉,我本意是这样做的。我不知道是否应该发布辅助函数?主要部分我想看看如何使用虚假数据模拟该URL调用。 - Kyle Bridenstine
你可以使用在本地运行的Web服务器,而不是Mockito模拟。 - David Ehrmann
我该如何将这个封装成一个单一的项目,以便其他开发人员能够长期复制我的测试呢?它总是会成为两个独立的应用程序,对吗? - Kyle Bridenstine
你可以在单元测试中轻松启动一个HTTP服务器。请查看http://undertow.io/。 - benez
3个回答

4

问题的一部分在于您没有将事物分解为接口。您需要将 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返回一个模拟值(或抛出异常)。

这太完美了。这实际上就是我应该编写代码的方式...谢谢你。我不知道为什么我没有想到这个。 - Kyle Bridenstine
1
这种方法激励程序员修改他们的代码,以便测试可以注入或覆盖行为。请记住,这也会修改您的导出 API(在本例中为受保护的方法)。我会尽量避免不得不修改我的 API 以使其“可测试”。您可能会意外地暴露程序的某些部分。 - benez

3
这里的其他答案建议您重构代码,使用一种提供程序,您可以在测试期间替换它 - 这是更好的方法。
如果由于某种原因这不是可能的,您可以安装一个自定义的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并返回什么内容的标准相当简单。


1
答案取决于您正在测试什么。 如果需要测试InputStream的处理 如果某些代码调用getContent()来处理InputStream返回的数据,并且您想测试处理代码如何处理特定的输入集,则需要创建一个接缝以启用测试。我会简单地将getContent()移动到一个新类中,并将该类注入到执行处理的类中:
public interface ContentSource {

  InputStream getContent(String uri);
}

你可以创建一个HttpContentSource,它使用URL.openConnection() (或者更好的是使用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服务器,当你的测试创建系统时,配置它与该服务器通信。参见this question的答案以了解如何启动HTTP服务器。

  2. 配置Spring使用ContentSource的虚拟实现。这样做可以获得稍微少一些端到端的可靠性,但速度更快、更不容易出错。


最终我要测试程序的其余部分。为了测试程序的其余部分,我想将预定义的xml文件作为输入提供给它,并检查程序的输出以确保它处理一切都很好。为此,我将我的问题缩小到这个getContent()函数。如果我能找到一种拦截HTTP GET请求并注入自己的xml文件的方法,那么我就能确定最终结果。我不知道如何创建端到端测试或设置内存服务器。你能给我指一个例子吗?如果可以的话,我正在使用Spring Boot、Maven和Netbeans。谢谢。 - Kyle Bridenstine
如果你正在使用Spring,那么你可以创建一个配置文件来将大部分对象连接在一起,但是用一些由你控制的虚拟版本替换其中的一些对象。 - NamshubWriter
关于如何设置内存服务器,有很多可能性。我喜欢Python的SimpleHTTPServer类。如果您想要Java解决方案,请参见https://dev59.com/02865IYBdhLWcg3wnPya中的答案。 - NamshubWriter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接