下面的测试类验证简单的HttpService是否从给定的URL获取内容。所示的两个实现都会使测试通过,但有一个显然是错误的,因为它使用不正确的参数构造URL。
为了避免这种情况并正确指定我想要的行为,我想验证在测试用例的使用块中,我构造了一个(并且只有一个)URL类的实例,并且{{1构造函数的参数是正确的。 A Groovy enhancement似乎会让我添加语句
url
但如果没有Groovy增强功能,我该怎么办?
更新:在标题中用“stub”替换“mock”,因为我只对检查状态不一定是交互的细节感兴趣。 Groovy有一个我没有使用的StubFor机制,所以我将保留我的代码,但我认为你可以在整个过程中用StubFor替换MockFor。
mockURLContext.demand.URL { assertEquals "http://www.foo.com", url }
import grails.test.*
import groovy.mock.interceptor.MockFor
class HttpServiceTests extends GrailsUnitTestCase {
void testGetsContentForURL() {
def content = [text : "<html><body>Hello, world</body></html>"]
def mockURLContext = new MockFor(URL.class)
mockURLContext.demand.getContent { content }
mockURLContext.use {
def httpService = new HttpService()
assertEquals content.text, httpService.getContentFor("http://www.foo.com")
}
}
}
// This is the intended implementation.
class HttpService {
def getContentFor(url) {
new URL(url).content.text
}
}
答案 0 :(得分:3)
模拟URL可以获得什么?这使得测试难以编写。您将无法对模拟对象为您提供有关URL类API的设计的反馈做出反应,因为它不受您的控制。如果你没有准确地伪造URL的行为以及它暴露的有关HTTP协议的内容,那么测试将不可靠。
您希望测试您的“HttpService”对象是否实际从给定的URL正确加载数据,正确处理不同的内容类型编码,适当地处理不同类别的HTTP状态代码,等等。当我需要测试这种类型的对象 - 一个只包含一些底层技术基础架构的对象时 - 我编写了一个真正的集成测试,它验证了该对象确实正确地使用了底层技术。
对于HTTP,我编写了一个创建HTTP服务器的测试,将servlet插入服务器,返回一些预制数据,将servlet的URL传递给对象以使其加载数据,检查加载的结果是与用于初始化servlet的固定数据相同,并在夹具拆卸中停止服务器。我使用Jetty或与JDK 6捆绑在一起的简单HTTP服务器。
我只使用模拟对象来测试与我集成测试的对象的接口对话的对象的行为。
答案 1 :(得分:2)
加上我的“小编程”和“单元测试100%”,你可以认为这是一个做太多事情的单一方法。您可以将HttpService重构为:
class HttpService {
def newURLFrom(urlString) {
new URL(urlString)
}
def getContentText(url) {
url.content.text
}
def getContentFor(urlString) {
getContentText(newURLFrom(urlString))
}
}
这将为您提供更多测试选项,以及从属性操作中分离出工厂方面。测试选项比较普通:
class HttpServiceTests extends GroovyTestCase {
def urlString = "http://stackoverflow.com"
def fauxHtml = "<html><body>Hello, world</body></html>";
def fauxURL = [content : [text : fauxHtml]]
void testMakesURLs() {
assertEquals(urlString,
new HTTPService().newURLFrom(urlString).toExternalForm())
}
void testCanDeriveContentText() {
assertEquals(fauxHtml, new HTTPService().getContentText(fauxURL));
}
// Going a bit overboard to test the line combining the two methods
void testGetsContentForURL() {
def service = new HTTPService()
def emc = new ExpandoMetaClass( service.class, false )
emc.newURLFrom = { input -> assertEquals(urlString, input); return fauxURL }
emc.initialize()
service.metaClass = emc
assertEquals(fauxHtml, service.getContentFor(urlString))
}
}
我认为这会产生你想要的所有断言,但代价是在最后一种情况下牺牲了测试可读性。
我同意Nat的观点,认为这更符合集成测试。 (您正在某种程度上与Java的URL库集成。)但假设此示例简化了一些复杂的逻辑,您可以使用元类覆盖实例类,从而部分模拟实例。
答案 2 :(得分:0)
你到底想要失败的是什么?您尝试使用该代码测试的内容并不明显。通过模拟URL.getContent
,您告诉Groovy始终在content
时返回变量URL.getContent() is invoked
。您是否希望根据URL字符串使URL.getContent()
的返回值有条件?如果是这种情况,以下内容将实现:
import grails.test.*
import groovy.mock.interceptor.MockFor
class HttpServiceTests extends GrailsUnitTestCase {
def connectionUrl
void testGetsContentForURL() {
// put the desired "correct" URL as the key in the next line
def content = ["http://www.foo.com" : "<html><body>Hello, world</body></html>"]
def mockURLContext = new MockFor(URL.class)
mockURLContext.demand.getContent { [text : content[this.connectionUrl]] }
mockURLContext.use {
def httpService = new HttpService()
this.connectionUrl = "http://www.wrongurl.com"
assertEquals content.text, httpService.getContentFor(this.connectionUrl)
}
}
}
答案 3 :(得分:0)
很难模拟声明为final的JDK类......正如您通过增强功能引用的那样,您的问题是除了调用构造函数之外无法创建URL。
我尝试将这些对象的创建与其余代码分开;我创建了一个工厂来分隔创建URL。这应该足够简单,不能保证测试。其他人采用典型的包装/装饰方法。或者,您可以应用adapter pattern转换为您编写的域对象。
以下是令人惊讶的类似问题的类似答案:Mocking a URL in Java
我认为这证明了很多人在做了更多测试之后学到的东西:我们编写的代码使得事情更容易测试,这意味着将我们想要测试的东西与我们可以安全地说已经在其他地方测试过的东西隔离开来。这是我们为了进行单元测试而必须做出的基本假设。它还可以提供一个很好的例子,说明为什么良好的单元测试不一定是100%的代码覆盖率。它们也必须经济实惠。
希望这有帮助。