Coarse-grained testing with stubs
And yet it moves.——伽利略(Junit in action作者加)
本章内容:
- 介绍stub
- 使用嵌入式服务器代替真正的网络服务器
- 使用stub单元测试一个http链接案例
stub简介
但开发应用程序时,发现现在需要测试的类还要依赖于其他的类库,总之是一个没有完善的开发环境,则在实际开发中时经常遇见的情况,例如,你的应用程序使用http连接由第三方提供web服务器,但是在你开发环境下通常不存在那样的一个可用服务器程序,所以需要模拟服务器。还有一种情况就是和其他的开发者一起开发时,你想测试自己的那部分,但是其他的部分还没有完成。在这些情况下,stub为我们提供了解决方案!
定义 stub——stub是代码的一部分。在运行时我们用stub替换真正代码,忽略调用代码的实现。目的是用简单一点的行为替换一个真正的行为,从而允许独立的测试代码的某个部分。
使用stub的例子:
你不能修改一个现有的系统,因为它很复杂,很容易崩溃
粗粒度测试,如在不同的子系统之间进行集成测试
缺点:
stub通常是很复杂,他们本身需要调试
因为的复杂性,他们可能会很难维护
stub不能很好的运用于细粒度的测试
使用不同情况要不同的策略
一个HTTP连接的例子
为了演示stub能做些什么,我们为一个简单的程序创建一些stub,他根据URL打开一个Http连接,同时读取其中的信息。
实际情况:看不到图的朋友点这里!
我们假设远程web资源是个servlet,它以某种方法(我们说,调用一个JSP)产生HTML响应。
使用了stub的情况:
待测试代码
打开http示例的代码,也就是我们在后面进行测试的代码!
package junitbook.coarse.try1; import java.net.URL; import java.net.HttpURLConnection; import java.io.InputStream; import java.io.IOException; public class WebClient { public String getContent(URL url) { StringBuffer content = new StringBuffer(); try { HttpURLConnection connection = ;打开到URL的http连接 (HttpURLConnection) url.openConnection(); ; connection.setDoInput(true); ; InputStream is = connection.getInputStream(); ;开始读取远程数据 byte[] buffer = new byte[2048]; int count; while (-1 != (count = is.read(buffer))) ;把所有的数据都读入流中 { ; content.append(new String(buffer, 0, count)); ; } ; } catch (IOException e) { return null; ;出错时返回null } return content.toString(); } }
选择一种替换方案
在这个程序中可能有两种情况:远程web服务器位于开发平台的外围(如在合作站点上),或者,本身就是程序配置的平台的一部分。 不管怎样,为了能够对WebClient类进行单元测试,必须在开发平台上建立服务器!相对容易的方法就是为其安装一个Apache测试服务器,在它的文档根目录下放一些测试web页面。这是典型的、广泛使用的替换方法。但是缺点是非常明显的:
依赖环境——在测试前确保运行环境已经准备好了。如果web服务器关闭了,但测试被执行了,结果必然是错误的。
分散的测试逻辑——测试逻辑被分散到两个不同的地方:一是在JUnit test case,二是测试web页面。两种资源要同步。
测试难以实现自动化——自动测试还是很困难,因为它需要在web服务器上自动的配置web页面,自动启动web服务器,而 完成这一切仅仅是为了运行单元测试!
幸运的,有一个更好的解决方案——使用嵌入式服务器Jetty!关于Jetty的一般信息,访问http://jetty.mortbay.org/jetty/index.html
Jetty是轻量级的运行速度快的很好的web/servlet容器,使用Jetty可以消除前文的不足之处,服务器从JUnit test case开始运行,所有测试都在同一个位置用java编写,把test suite 自动化也成了一个微不足道的问题。得益于Jetty的模块性,做的事情只是用stub替换Jetty处理器,而不是替换整个服务器!
代码,以嵌入模式启动Jetty——JettySample类
package junitbook.coarse; import org.mortbay.http.HttpContext; import org.mortbay.http.HttpServer; import org.mortbay.http.SocketListener; import org.mortbay.http.handler.ResourceHandler; public class JettySample { public static void main(String[] args) throws Exception { HttpServer server = new HttpServer(); ;创建Jetty HttpServer对象 SocketListener listener = new SocketListener();;在端口8080上给HttpServer对象绑上一个listener,这样它可以接收http请求 listener.setPort(8080); server.addListener(listener); HttpContext context = new HttpContext(); ;建立一个HttpContext,处理HTTP请求,把这些请求分发到不同的处理器那里 context.setContextPath("/"); ;用setContextPath把context映像到根目录(/)URL上。 context.setResourceBase("./"); ;setResourceBase设置文档根目录以提供资源。 context.addHandler(new ResourceHandler()); ;添加资源处理器到HttpContext,使之能提供文件系统中的文件。 server.addContext(context); server.start(); ;启动服务器。 } }
替换Web服务器资源
建立第一个stub测试
为了证明WebClient工作于有效的URL,你需要在测试启动Jetty服务器,这些都能在JUnit test case 里的setUp方法中实现!也可以用tearDown方法停止服务器的运行!
package junitbook.coarse.try1; import java.io.IOException; import java.io.OutputStream; import junit.extensions.TestSetup; import junit.framework.Test; import org.mortbay.http.HttpContext; import org.mortbay.http.HttpFields; import org.mortbay.http.HttpRequest; import org.mortbay.http.HttpResponse; import org.mortbay.http.HttpServer; import org.mortbay.http.SocketListener; import org.mortbay.http.handler.AbstractHttpHandler; import org.mortbay.util.ByteArrayISO8859Writer; public class TestWebClientSetup1 extends TestSetup { protected static HttpServer server; public TestWebClientSetup1(Test suite) { super(suite); } protected void setUp() throws Exception { server = new HttpServer(); SocketListener listener = new SocketListener(); listener.setPort(8080); server.addListener(listener); HttpContext context1 = new HttpContext(); context1.setContextPath("/testGetContentOk"); context1.addHandler(new TestGetContentOkHandler()); server.addContext(context1); server.start(); } protected void tearDown() throws Exception { server.stop(); } private class TestGetContentOkHandler ;通过AbstractHttpHandler 类可以很容易的建立处理器 extends AbstractHttpHandler ;该类定义了一个简单的handle方法,你需要把它实现 { ;当收到的请求转到处理器上时,Jetty会调用这个方法 public void handle(String pathInContext, String pathParams, HttpRequest request, HttpResponse response) throws IOException { OutputStream out = response.getOutputStream(); ;使用由Jetty提供的ByteArrayISO8859Writer类,这样在 ByteArrayISO8859Writer writer = ;Http响应中返回你的字符就轻而易举了! new ByteArrayISO8859Writer(); writer.write("It works"); writer.flush(); response.setIntField( HttpFields.__ContentLength, writer.size()); ;设置相应内容的长度为写入输出流(这在Jetty中是必需的!)的字符长度 writer.writeTo(out); out.flush(); request.setHandled(true); ;告诉Jetty,请求已经被处理了,不需要传给任何更深层的处理了 } } }
看了上面代码中的setUp和tearDown方法,大家会很快的注意到,我们需要准备一个包含有文本“It works”的静态页面。把该文本放在你的文档根目录中。但是,这显然是非常麻烦的,另一种方式就是,配置Jetty以使用你自己的处理器,它不用在系统文件中取字符,而是直接返回一个字符串。这是一个更强大的技术,即使远程服务器在你的WebClient客户上抛出一个异常,你还是能使用!
经过上面的一番配置,你已经为测试做好了准备,你需要解决的最后一件事情就是优化setUp/tearDown方法。上面的做法并不好,因为它要为每个测试启动和停止。虽然,Jetty很快,但是这种处理仍然没有必要,更好的是执行所有的测试,但是服务器只启动一次!幸运的是JUnit支持TestSetup概念,可以做到这点!TestSetup把一堆测试封装成一个suite,这个suite有setUp和tearDown方法,作用域是suite内的所有测试。
每个testsuite启动和停止Jetty各一次
package junitbook.coarse.try1; import java.io.IOException; import java.io.OutputStream; import junit.extensions.TestSetup; import junit.framework.Test; import org.mortbay.http.HttpContext; import org.mortbay.http.HttpFields; import org.mortbay.http.HttpRequest; import org.mortbay.http.HttpResponse; import org.mortbay.http.HttpServer; import org.mortbay.http.SocketListener; import org.mortbay.http.handler.AbstractHttpHandler; import org.mortbay.http.handler.NotFoundHandler; import org.mortbay.util.ByteArrayISO8859Writer; public class TestWebClientSetup3 extends TestSetup ;继承TestSetup 类,就可以使用全局的testSetup和tearDown { protected static HttpServer server; ;创建一个Jetty服务器,在后面调用! public TestWebClientSetup3(Test suite) { super(suite); } protected void setUp() throws Exception { server = new HttpServer(); SocketListener listener = new SocketListener(); listener.setPort(8080); server.addListener(listener); HttpContext context1 = new HttpContext(); context1.setContextPath("/testGetContentOk"); ;从下面开始是三个处理器! context1.addHandler(new TestGetContentOkHandler()); server.addContext(context1); HttpContext context2 = new HttpContext(); context2.setContextPath("/testGetContentNotFound"); context2.addHandler(new NotFoundHandler()); server.addContext(context2); HttpContext context3 = new HttpContext(); context3.setContextPath("/testGetContentServerError"); context3.addHandler(new TestGetContentServerErrorHandler()); server.addContext(context3); server.start(); } protected void tearDown() throws Exception { server.stop(); } private class TestGetContentOkHandler extends AbstractHttpHandler { public void handle(String pathInContext, String pathParams, HttpRequest request, HttpResponse response) throws IOException { OutputStream out = response.getOutputStream(); ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer(); writer.write("It works"); writer.flush(); response.setIntField( HttpFields.__ContentLength, writer.size()); writer.writeTo(out); out.flush(); request.setHandled(true); } } private class TestGetContentServerErrorHandler extends AbstractHttpHandler { public void handle(String pathInContext, String pathParams, HttpRequest request, HttpResponse response) throws IOException { response.sendError( HttpResponse.__503_Service_Unavailable); } } }
package junitbook.coarse.try1; import java.net.URL; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; public class TestWebClient3 extends TestCase { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTestSuite(TestWebClient3.class); return new TestWebClientSetup3(suite); } public void testGetContentOk() throws Exception { WebClient client = new WebClient(); String result = client.getContent(new URL( "http://localhost:8080/testGetContentOk")); assertEquals("It works", result); } public void testGetContentNotFound() throws Exception { WebClient client = new WebClient(); String result = client.getContent(new URL( "http://localhost:8080/testGetContentNotFound")); assertNull(result); } public void testGetContentServerError() throws Exception { WebClient client = new WebClient(); String result = client.getContent(new URL( "http://localhost:8080/testGetContentServerError")); assertNull(result); } }
回顾第一个stub
到这里就完成了很多的工作!对方法进行了单元测试而且还进行了集成测试,此外还检验了代码的逻辑,还测试了代码之外的连接部分。不足之处就是太复杂!
更大的不足之处就是,在这个例子中我们需要一个web服务器,另外的一个stub例子就不一样了,它需要不同的配置,经验会有所帮助,但是不同的情况需要不同的方案。
更好的stub:替换链接
现在我们是替换了web服务器的资源。改为替换HTTP连接又会怎样呢?这样做会妨碍你有效的测试连接,但是,这又怎样呢?因为这一点不是我们真正的目标,我们真正感兴趣的是孤立的测试代码逻辑,在以后可以用功能测试或是集成测试来检验连接。但我们不想改代码就进行测试的时候,你就会发现自己很幸运,因为JDK的URL和HttpURLConnection类允许我们引入自定义的协议处理器,处理任何类型的通信协议。你可以使任何对HttpURLConnection类调用指向你自己的测试类,这些类会返回测试中需要的任何东西。
创建自定义的URL协处理器
为了实现自定义的URL协处理器,要调用以下JDK方法,并把它传递给自定义的URLStreamHandlerFactory对象:
java.net.URL.setURLStreamHandlerFactory( java.net.URLStreamHandlerFactory);
无论何时,只要调用一个URL.openConnection方法,URLStreamHandlerFactory类就被调用,返回一个URLStreamHandler对象。下面的代码给出了具体的实现,其想
就是在JUnit的setUp方法中调用静态的setsetURLStreamHandlerFactory方法。(更好的实现要用到前面讲的TestSetup类,以使它在testsuite执行期间只运行一次)
package junitbook.coarse.try2; import junit.framework.TestCase; import java.net.URL; import java.net.URLStreamHandlerFactory; import java.net.URLStreamHandler; import java.net.URLConnection; import java.io.IOException; public class TestWebClient extends TestCase { protected void setUp() { URL.setURLStreamHandlerFactory( ;告诉URL类用工厂的方法来处理 new StubStreamHandlerFactory()); } private class StubStreamHandlerFactory ;把所有连接路由到HTTP处理器 implements URLStreamHandlerFactory { public URLStreamHandler createURLStreamHandler( String protocol) { return new StubHttpURLStreamHandler(); } } private class StubHttpURLStreamHandler ;提供返回stub HttpURLConnection类的处理器 extends URLStreamHandler { protected URLConnection openConnection(URL url) throws IOException { return new StubHttpURLConnection(url); } } public void testGetContentOk() throws Exception ;检查WebClient类的测试 { WebClient client = new WebClient(); String result = client.getContent( new URL("http://jakarta.apache.org")); assertEquals("It works", result); } }
创建JDK的HttpURLConnection stub
最后一步是创建一个HttpURLConnection类的stub实现,这样你就能返回测试时想要的任何值。
package junitbook.coarse.try2; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.io.InputStream; import java.io.IOException; import java.io.ByteArrayInputStream; public class StubHttpURLConnection extends HttpURLConnection ;HttpURLConnection 没有接口,继承它,复写它的方法 { private boolean isInput = true; protected StubHttpURLConnection(URL url) { super(url); } public InputStream getInputStream() throws IOException ;复写getInputStream方法,返回字符串 { if (!isInput) { throw new ProtocolException( "Cannot read from URLConnection" + " if doInput=false (call setDoInput(true))"); } ByteArrayInputStream bais = new ByteArrayInputStream( new String("It works").getBytes()); ;预期的字符串作为返回值 return bais; } public void disconnect() { } public void connect() throws IOException { } public boolean usingProxy() { return false; } }
至此就可以运行测试了!
总结:在这一章里,我们示范了如何使用stub策略对访问远程web服务器的代
码进行了单元测试。我们使用了Jetty代替了远程的web服务器。Jetty的可嵌
如特性使我们专注于替换Jetty的http处理器,而不是整个容器。我们也为JDK
的HttpURLConnection写了stub,从而演示了更轻量的stub。