尝试2:使用类工厂进行重构
我们采用Inversion of Control(IoC)模式,这个模式要求用到的任何的资源都要传给getContent方法或WebClient类。现在我们拥有的唯一的资源是HttpURLConnection对象。你可以把WebClient.getContent签名改为
public String getContent(URL url,HttpURLConnection connection)
这表示你将HttpURLConnection对象建立任务传递给了WebClient的调用者。然而,URL是从HttpURLConnection类得到的,这个签名看上去也不是很好。幸运的是,有一个更好的方法创建ConnectionFactory接口。凡是实现ConnectionFactory接口的类都从连接中返回InputStream,无论连接是何种类型(Http/TCP/IP等)。这种重构的方式叫做Class Factory。
代码ConnectionFactory.java
package junitbook.fine.try2;
import java.io.InputStream;
public interface ConnectionFactory
{
InputStream getData() throws Exception;
}
WebClient的代码实现如下。
这种办法更好,因为你获取数据内容的方式独立于你用到的连接的类型。最初是用HTTP协议在URLs上工作的。现在可以实现在任何标准协议上运行,甚至是你自己定义的协议。下面的代码实现了HTTP协议的ConnectionFactory实现。
HttpURLConnectionFactory.java
package junitbook.fine.try2; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; public class HttpURLConnectionFactory implements ConnectionFactory { private URL url; public HttpURLConnectionFactory(URL url) { this.url = url; } public InputStream getData() throws Exception { HttpURLConnection connection = (HttpURLConnection) this.url.openConnection(); return connection.getInputStream(); } }
现在你可以通过为ConnectionFactory编写mock来很容易的测试getContent方法。
package junitbook.fine.try2; import java.io.InputStream; public class MockConnectionFactory implements ConnectionFactory { private InputStream inputStream; public void setData(InputStream stream) { this.inputStream = stream; } public InputStream getData() throws Exception { return this.inputStream; } }
通常,mock不包含任何逻辑,完全由外界控制(通过调用setData方法)你可以使用MockConnectionFactory重写测试。
package junitbook.fine.try2; import java.io.ByteArrayInputStream; import junit.framework.TestCase; public class TestWebClient extends TestCase { public void testGetContentOk() throws Exception { MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); mockConnectionFactory.setData( new ByteArrayInputStream("It works".getBytes())); WebClient client = new WebClient(); String result = client.getContent(mockConnectionFactory); assertEquals("It works", result); } }
现在就已经达成一开始的目标:对WebClient.getContent方法的代码逻辑进行了单元测试。在这个过程中,为了测试而重构了该方法。这导致其他实现具有更佳的扩展性,更能适应变化。
------------------------------------------------------------------------ -------------------------------------------------------------------------------
把mock objects用作特洛伊木马
mock是特洛伊木马,但是没有恶意。Mocks从内部替代真正的对象,调用类。可以把mocks用作探测器,监视待测试的对象的方法调用。以Http连接为例,你想要监视的一个方法为InputStream的close方法。目前你还没有对InputStream使用mock object,但是你可以很容易的创建一个mock object,提供一个verify方法来确认close方法被调用。
然后你可以在测试结束时调用verify方法,检查所有应该被调用的方法是否都被调用了。也许你还想看看close是不是只调用了一次,若调用了不止一次或没有调用,要抛出异常。
定义: 预期---但我们在谈论mock objects时,预期是mock内部的一项特性,它检验调用mock的外部类有没有出错。例如,数据库连接mock时,在任何使用mock的测试中,都要查一下close方法是否只调用了一次。
关闭时所有预期的Mock InputStream
package junitbook.fine.expectation; import java.io.IOException; import java.io.InputStream; import junit.framework.AssertionFailedError; public class MockInputStream extends InputStream { private String buffer; private int position = 0; private int closeCount = 0; public void setBuffer(String buffer) { this.buffer = buffer; } public int read() throws IOException { if (position == this.buffer.length()) { return -1; } return this.buffer.charAt(this.position++); } public void close() throws IOException { closeCount++; super.close(); } public void verify() throws AssertionFailedError { if (closeCount != 1) { throw new AssertionFailedError("close() should " + "have been called once and once only"); } } }
接着还要修改TestWebClient.testGetContentOk的测试方法。使用新的MockInputStream:
package junitbook.fine.expectation; import junit.framework.TestCase; import junitbook.fine.try2.MockConnectionFactory; public class TestWebClient extends TestCase { public void testGetContentOk() throws Exception { MockConnectionFactory mockConnectionFactory = new MockConnectionFactory(); MockInputStream mockStream = new MockInputStream(); mockStream.setBuffer("It works"); mockConnectionFactory.setData(mockStream); WebClient client = new WebClient(); String result = client.getContent(mockConnectionFactory); assertEquals("It works", result); mockStream.verify(); } }
在前面的WebClient中还要进行修改,因为原来的代码中没有定义从页面返回的输入流的关闭,所以在测试的时候会发生错误。修改后的
WebClient
package junitbook.fine.expectation; import java.io.IOException; import java.io.InputStream; import junitbook.fine.try2.ConnectionFactory; public class WebClient { public String getContent(ConnectionFactory connectionFactory) throws IOException { String result; StringBuffer content = new StringBuffer(); InputStream is = null; try { is = connectionFactory.getData(); byte[] buffer = new byte[2048]; int count; while (-1 != (count = is.read(buffer))) { content.append(new String(buffer, 0, count)); } result = content.toString(); } catch (Exception e) { result = null; } // Close the stream if (is != null) { try { is.close(); } catch (IOException e) { result = null; } } return result; } }
对于预期,还有其他的方便的使用方法。可以认为,除了为你测试时想要的行为之外,mock还可以就其使用情况提供有用的反馈。
注意:MockObjects项目(http://www.mockobjects.com)为标准的JDKAPI提供了一些现成的mock objects。这些mock通常内置有预期。此外,MockObjects项目还提供了一些有用的预期类。
------------------------------------------------------------------------ -------------------------------------------------------------------------------
决定何时用Mock Objects
- 真实的对象没有预期的行为
- 真实的对象难以配置
- 真实对象的一些行为难以控制其发生
- 真实对象运行较慢
- 真实对象具有用户界面
- 测试时要查询对象,但是无法查询真实对象
- 真实对象上尚不存在
小结
本章描述了mock object技法,这种方法帮助我们隔离了其他领域对象以及环境而对代码进行单元测试。当涉及编写细粒度的单元测试的时候,一个主要的障碍就是从执行的环节中抽象出来。我们常常听到这样的说法:“我没有测试过这个方法,因为它模拟了一个真实的环境实在是太难了!”恩,这样的说法不再成立了!(Poker face…)
在大多数的情况下,编写mock objects测试有一个正面的副作用:它迫使你重写部分测试代码。在实践中通常代码写的不是很好。你常常不必要的把类之间的耦合关系以及和环境之间的耦合关系写进了代码之中。这很容易写出难以在不同的环境中复用的代码,一点点的失误可能对系统的其它类有很大的影响。而通过mock objects,你必须从不同的角度考虑代码,并且应用很好的设计模式,比如Interface和IOC。
不应当仅仅把mock objects看作是单元测试技术,它还是一项设计技术。称作测试驱动开发的一颗正在升起的方法学明星就提倡在编写代码之前编写测试。使用TDD,你不需要为了可以测试而重构代码,你编写的代码已经是可以测试的!
但是,编写mock objects虽然简单,但是你需要在为几百个对象编写mock时这会令人厌烦。下面的章节中我们会介绍几个开源框架,他们可以为你的类自动生成可以使用的mocks,使得通过mock objects策略成为一件令人愉快的事!