为了了解mock objects在实际例子中是如何工作的,我们以一个简单的应用程序为例,它打开一个远程服务器的HTTP连接,然后读取页面内容。在第6章中,我们使用了stub测试这个应用程序。现在我们使用mock objects来模拟HTTP连接,来对其进行单元测试。
另外,你将会学习到如何为没有Java接口的类(即 HttpURLConnection类)编写mock。我们将会向读者展示一个完整的场景:从一个初始的测试实现开始,到进一步改善实现,以及修改原始代码使之更加灵活。并且我们还会展示如何使用mocks来测试错误情况。
随着学习的不断深入,你将会不断地改善测试代码以及示例应用程序,尤其是针对同一个应用程序使用不同的方法编写单元测试。在这个过程中,你将会学会如何实现一个简单而优雅的测试方案,同时又让应用程序代码更加灵活、易于更改。
图7.2 显示了一个HTTP示例应用程序。
图7.2 引入测试之前的HTTP示例应用程序
这个应用程序包含一个简单的webclient.getContent方法,来对 Web服务器上所运行的Web资源进行HTTP 连接。我们希望在隔离Web资源的情况下对getcontent方法进行单元测试。
图7.3展示了一个mock object 的定义。MockURL类代表了真正的URL类。在getcontent里面所有对URL类的调用都会指向MockURL类。你可以看到,测试其实是一个控制器:它创建并配置mock在此测试中所要完成的行为;它会(由于某些原因)使用MockURL类替代真正的URL类,然后再运行测试。
图7.3 在一个使用mock objects进行测试的详细步骤
图7.3展示了mock objects策略非常有趣的一面:在生产代码中需要能够被mock替换。细心的读者可能已经注意到,URL类是 final类型的,因此不太可能创建一个扩展的 MoCkURL类。
在接下来的章节里,我们将展示如何使用不同的方式实现这一技巧(通过在另一个级别上使用mock)。在任何情况下,当使用mock objects 策略时,将真正的类替换成mock是一个难点。这也许可以被视为mock objects的一个缺点,因为通常我们需要修改代码以提供一个暗门。不过具有讽刺意味的是,修改代码以提高灵活性同时也是使用 mock的优势之一,正如7.3.1小节所描述的那样。
代码7.6中的示例展示了一个代码片段,它打开了一个到给定URL 的HTTP连接,然后读取了在 URL上的内容。让我们想象一下,这是我们要对其进行单元测试的大型应用程序中的一个方法,我们现在来测试这个方法吧。
如果有错误发生,将会返回null。当然,这并不是所有可能的错误处理办法中最好的,但是就目前而言,这已经足够好了(之后的测试将会鼓励我们去重构代码的)。
我们的想法就是能够不依赖于一个真正的、到 Web服务器的HTTP连接,独立地测试getContent方法。如果回忆一下7.2节所学到的知识,那么就意味着要编写一个mockURL,在其中,url.openConnection方法将会返回一个mock HttpURLConnection。MockHttpURLConnection类将会提供一个实现,让测试来决定getInputstream方法返回什么样的内容。理想情况下,你能够写出如下的测试:
不幸的是,这个解决方案并没有用!因为JDK URL类是一个final类,并且没有任何可用的URL接口。所以扩展性仅此而已。我们需要找出另一个解决方案,很可能是去mock另外一个对象。其中的一个解决方案是替换URLStreamHandlerFactory类。这个方案我们已经在第6章探讨过了,因此这里我们来找出一种是使用mock objects的解决方案:重构getcontent方法。如果思考一下,就会发现这个方法完成了两件事情:获取一个HttpURLConnection对象然后从中读取内容。可以将其重构,重构的结果就是如代码7.7所示的类(相对代码7.6更改的部分已用粗体显示)。我们截取了获取HttpURLConnection对象的那一部分代码。
在上面的代码中,我们调用了createHttpURLConnection来创建HTTP连接。
这个解决方案如何使测试getContent更加有效呢?我们现在可以使用一个很有用的技巧,那就是编写一个继承webclient类的测试辅助类,然后覆写它的createHttpURLconnection方法,如下所示:
在测试中,我们可以调用setHttpURLConnection方法,并把它传递给 mock
HttpURLConnection对象。现在测试代码就变成了以下效果(更改部分显示为粗体):
在以上代码中,我们配置了'TestableWebClient,以便createHttpURLConnection方法返回一个mock对象。接下来再调用getcontent方法。
这是一个常用的重构方法,也称为方法工厂 (method factory)重构,尤其是在需要mock的类没有接口时这个方法非常有用。策略就是继承这个类,添加一些setter方法来控制它,并且覆写它的某些getter方法,以便返回测试中我们想要的东西。对当前这个例子而言,此方法还是不错的,但是并不完美。这有点像海森堡测不准原理(Heisenberg uncertainty principle) :被测试的子类改变了它的行为,所以当我们测试子类的时候,能放心地进行测试吗?
这个技巧对打开一个对象使之更适合测试而言是很有用的,但若仅止于此,那么它其实就是测试了一个与原始待测类相似的东西(并不是完全相同)。当然,我们并不是在为第三方库编写测试而不能更改代码——我们对待测代码有完全的控制权。因此我们可以改善代码,使之在整个过程中更易于测试。
让我们来应用控制反转(loC)模式,也就是说我们使用的任何资源都需要传给getContent方法或者 webclient类。这里唯一使用的资源是HttpURLConnection对象。我们可以把 webclient.getContent签名更改为:
这也就意味着我们将HttpURLConnection对象的创建任务推给了webclient的调用者。但是,URL是从HttpURLConnection类获取的,并且这个签名看起来并不是很好。幸运的是,有一个更好的方法创建了一个ConnectionFactory接口,如代码7.8与代码7.9所示。凡是实现 ConnectionFactory接口的类都从一个连接返回一个InputStream,无论连接的类型是什么(HTTP、TCP/IP等)。这种重构技术有时候被称为类工厂重构'。
webclient的实现如代码7.9所示(与初始代码7.6相比,更改的地方已用粗体表示)。
这个方案更好一些,因为我们使得数据内容的提取独立于我们获取连接的方式。最初的实现只能使用基于HTTP协议的URL。但是新的实现则可以使用任何标准的协议(file:/l、http://、ftp://、jar://等),甚至是你自己自定义的协议。例如,代码7.10展示了基于HTTP协议的ConnectionFactory实现。
现在我们可以通过给ConnectionFactory编写一个mock来轻松测试getcontent方法(参考代码7.11)。
通常, mock并不包含任何逻辑,完全由外部控制(通过调用setData方法来控制)。现在我们可以轻松地使用MockconnectionFactory来重写测试,如代码7.12所示。
我们已经实现了最初的目标:对 webclient.getContent方法的代码逻辑进行单元测试。在这个过程中,我们为了测试不得不对它进行重构,这将会使得它的实现具有更好的扩展性,更能适应变化。