我正在尝试为我的程序编写单元测试并使用模拟数据。我对如何拦截对 URL 的 HTTP Get 请求有点困惑。
我的程序调用我们 API 的 URL,它返回一个简单的 XML 文件。我希望测试而不是从 API 在线获取 XML 文件,而是从我那里接收预定的 XML 文件,以便我可以将输出与预期输出进行比较并确定一切是否正常工作。
我被指向 Mockito,并且已经看到了许多不同的例子,例如这篇 SO 帖子,如何使用 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框架的其他部分,如果有帮助的话。
部分问题在于你没有将事情分解为接口。您需要将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
实现。
这里的其他答案建议你重构你的代码,使用一种可以在测试期间替换的提供程序 - 这是更好的方法。
如果出于某种原因无法做到这一点,您可以安装一个自定义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 以及返回哪些内容的标准非常容易。
答案取决于您正在测试的内容。
如果您需要测试输入流的处理
如果getContent()
是由处理InputStream
返回的数据的某些代码调用的,并且您想测试处理代码如何处理特定的输入集,那么您需要创建一个接缝来启用测试。我只需将getContent()
移动到一个新类中,并将该类注入到执行处理的类中:
public interface ContentSource {
InputStream getContent(String uri);
}
你可以创建一个使用URL.openConnection()
(或者更好的是,Apache HttpClientcode)的HttpContentSource
。
然后,您将ContentSource
注入处理器:
public class Processor {
private final ContentSource contentSource;
@Inject
public Processor(ContentSource contentSource) {
this.contentSource = contentSource;
}
...
}
Processor
中的代码可以用模拟ContentSource
进行测试。
如果您需要测试内容的获取
如果要确保getContent()
正常工作,可以创建一个测试,该测试启动提供预期内容的轻量级内存中 HTTP 服务器,并getContent()
与该服务器通信。这似乎有些矫枉过正。
如果您需要使用虚假数据测试系统的大部分子集
如果你想确保事情端到端地工作,写一个端到端的系统测试。由于您指示使用 Spring,因此可以使用 Spring 将系统的各个部分连接在一起(或将整个系统连接在一起,但具有不同的属性)。你有两个选择
让系统测试启动本地 HTTP 服务器,并在测试创建系统时将其配置为与该服务器通信。有关启动 HTTP 服务器的方法,请参阅此问题的答案。
配置 spring 以使用
ContentSource
的虚假实现。这会让你对一切都端到端工作的信心略低,但它会更快,更少片状。