JUnit学习笔记6---用stub进行粗粒度测试

Coarse-grained testing with stubs

                                                                                                                              And yet it moves.——伽利略(Junit in action作者加)

本章内容:

  1. 介绍stub
  2. 使用嵌入式服务器代替真正的网络服务器
  3. 使用stub单元测试一个http链接案例

stub简介

但开发应用程序时,发现现在需要测试的类还要依赖于其他的类库,总之是一个没有完善的开发环境,则在实际开发中时经常遇见的情况,例如,你的应用程序使用http连接由第三方提供web服务器,但是在你开发环境下通常不存在那样的一个可用服务器程序,所以需要模拟服务器。还有一种情况就是和其他的开发者一起开发时,你想测试自己的那部分,但是其他的部分还没有完成。在这些情况下,stub为我们提供了解决方案!

定义  stub——stub是代码的一部分。在运行时我们用stub替换真正代码,忽略调用代码的实现。目的是用简单一点的行为替换一个真正的行为,从而允许独立的测试代码的某个部分。

使用stub的例子:

 你不能修改一个现有的系统,因为它很复杂,很容易崩溃

粗粒度测试,如在不同的子系统之间进行集成测试

缺点:

 stub通常是很复杂,他们本身需要调试

因为的复杂性,他们可能会很难维护

stub不能很好的运用于细粒度的测试

使用不同情况要不同的策略

一个HTTP连接的例子

为了演示stub能做些什么,我们为一个简单的程序创建一些stub,他根据URL打开一个Http连接,同时读取其中的信息。

实际情况:看不到图的朋友点这里!

未命名

我们假设远程web资源是个servlet,它以某种方法(我们说,调用一个JSP)产生HTML响应。

使用了stub的情况:

1

待测试代码

  打开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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值