Mocking/Testing HTTP 获取请求
Mocking/Testing HTTP Get Request
我正在尝试为我的程序编写单元测试并使用模拟数据。我对如何拦截对 URL.
的 HTTP Get 请求有点困惑
我的程序调用 URL 到我们的 API,它是 returned 一个简单的 XML 文件。我希望测试不是从 API 在线获取 XML 文件,而是从我这里接收预定的 XML 文件,以便我可以将输出与预期输出进行比较并确定是否一切正常。
我被指向了 Mockito 并看到了许多不同的例子,例如这个 SO post,How to use mockito for testing a REST service? 但我不清楚如何设置它以及如何模拟数据(即,每当调用 URL 时,return 我自己的 xml 文件)。
我唯一能想到的是让另一个程序在 Tomcat 本地制作 运行 并且在我的测试中通过一个特殊的 URL 调用本地 运行 在 Tomcat 上编程,然后 return 我想测试的 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 启动和 Spring 框架的其他部分。
答案取决于您测试的内容。
如果需要测试InputStream的处理
如果 getContent()
被一些处理 InputStream
返回的数据的代码调用,并且您想测试处理代码如何处理特定的输入集,那么您需要创建一个接缝以启用测试。我只是将 getContent()
移动到一个新的 class 中,然后将 class 注入到执行处理的 class 中:
public interface ContentSource {
InputStream getContent(String uri);
}
您可以创建一个使用 URL.openConnection()
(或者,更好的是,Apache HttpClient 代码)的 HttpContentSource
。
然后你将 ContentSource
注入处理器:
public class Processor {
private final ContentSource contentSource;
@Inject
public Processor(ContentSource contentSource) {
this.contentSource = contentSource;
}
...
}
Processor
中的代码可以用 mock ContentSource
进行测试。
如果需要测试内容的抓取
如果您想确保 getContent()
有效,您可以创建一个测试来启动一个轻量级的内存中 HTTP 服务器来提供预期的内容,并让 getContent()
与该服务器通信.这似乎有些过分了。
如果需要用假数据测试系统的较大子集
如果你想确保端到端的工作,写一个端到端的系统测试。由于您表示您使用 Spring,因此您可以使用 Spring 将系统的各个部分连接在一起(或连接整个系统,但具有不同的属性)。你有两个选择
让系统测试启动一个本地 HTTP 服务器,当您让测试创建您的系统时,将其配置为与该服务器通信。查看 this question 的答案以了解启动 HTTP 服务器的方法。
配置 spring 以使用 ContentSource
的假实现。这会让您对一切端到端工作的信心略有下降,但它会更快,更稳定。
部分问题在于您没有将事物分解为接口。您需要将 getContent
包装到一个接口中,并提供一个具体的 class 实现该接口。这个具体 class 然后
需要传递给任何使用原始 getContent
的 class。 (这本质上是 依赖倒置 。)你的代码最终会看起来像这样。
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
(但没有太多要测试的),那么你可以创建一个派生的 class,它提供 stringToUrl
的实现 returns 模拟(或抛出异常)。
此处的其他答案建议您重构代码以使用一种可以在测试期间替换的提供程序 - 这是更好的方法。
如果出于某种原因无法做到这一点,您可以安装一个自定义 URLStreamHandlerFactory
拦截您想要 "mock" 的 URL 并回退到不应该的 URL 的标准实现被拦截。
请注意,这是不可逆的,因此 InterceptingUrlStreamHandlerFactory
一旦安装就无法删除 - 删除它的唯一方法是重新启动 JVM。您可以在其中实施一个标志以禁用它,并且 return 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();
}
}
}
虚拟主机.com/message.txt:
Hello World!
当运行时,本应用会输出:
Hello World!
{
"user-agent": "Java/1.8.0_45"
}
更改决定拦截哪些 URL 以及 return 的标准非常容易。
我正在尝试为我的程序编写单元测试并使用模拟数据。我对如何拦截对 URL.
的 HTTP Get 请求有点困惑我的程序调用 URL 到我们的 API,它是 returned 一个简单的 XML 文件。我希望测试不是从 API 在线获取 XML 文件,而是从我这里接收预定的 XML 文件,以便我可以将输出与预期输出进行比较并确定是否一切正常。
我被指向了 Mockito 并看到了许多不同的例子,例如这个 SO post,How to use mockito for testing a REST service? 但我不清楚如何设置它以及如何模拟数据(即,每当调用 URL 时,return 我自己的 xml 文件)。
我唯一能想到的是让另一个程序在 Tomcat 本地制作 运行 并且在我的测试中通过一个特殊的 URL 调用本地 运行 在 Tomcat 上编程,然后 return 我想测试的 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 启动和 Spring 框架的其他部分。
答案取决于您测试的内容。
如果需要测试InputStream的处理
如果 getContent()
被一些处理 InputStream
返回的数据的代码调用,并且您想测试处理代码如何处理特定的输入集,那么您需要创建一个接缝以启用测试。我只是将 getContent()
移动到一个新的 class 中,然后将 class 注入到执行处理的 class 中:
public interface ContentSource {
InputStream getContent(String uri);
}
您可以创建一个使用 URL.openConnection()
(或者,更好的是,Apache HttpClient 代码)的 HttpContentSource
。
然后你将 ContentSource
注入处理器:
public class Processor {
private final ContentSource contentSource;
@Inject
public Processor(ContentSource contentSource) {
this.contentSource = contentSource;
}
...
}
Processor
中的代码可以用 mock ContentSource
进行测试。
如果需要测试内容的抓取
如果您想确保 getContent()
有效,您可以创建一个测试来启动一个轻量级的内存中 HTTP 服务器来提供预期的内容,并让 getContent()
与该服务器通信.这似乎有些过分了。
如果需要用假数据测试系统的较大子集
如果你想确保端到端的工作,写一个端到端的系统测试。由于您表示您使用 Spring,因此您可以使用 Spring 将系统的各个部分连接在一起(或连接整个系统,但具有不同的属性)。你有两个选择
让系统测试启动一个本地 HTTP 服务器,当您让测试创建您的系统时,将其配置为与该服务器通信。查看 this question 的答案以了解启动 HTTP 服务器的方法。
配置 spring 以使用
ContentSource
的假实现。这会让您对一切端到端工作的信心略有下降,但它会更快,更稳定。
部分问题在于您没有将事物分解为接口。您需要将 getContent
包装到一个接口中,并提供一个具体的 class 实现该接口。这个具体 class 然后
需要传递给任何使用原始 getContent
的 class。 (这本质上是 依赖倒置 。)你的代码最终会看起来像这样。
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
(但没有太多要测试的),那么你可以创建一个派生的 class,它提供 stringToUrl
的实现 returns 模拟(或抛出异常)。
此处的其他答案建议您重构代码以使用一种可以在测试期间替换的提供程序 - 这是更好的方法。
如果出于某种原因无法做到这一点,您可以安装一个自定义 URLStreamHandlerFactory
拦截您想要 "mock" 的 URL 并回退到不应该的 URL 的标准实现被拦截。
请注意,这是不可逆的,因此 InterceptingUrlStreamHandlerFactory
一旦安装就无法删除 - 删除它的唯一方法是重新启动 JVM。您可以在其中实施一个标志以禁用它,并且 return 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();
}
}
}
虚拟主机.com/message.txt:
Hello World!
当运行时,本应用会输出:
Hello World!
{
"user-agent": "Java/1.8.0_45"
}
更改决定拦截哪些 URL 以及 return 的标准非常容易。