读书笔记 -- Junit 实战(3rd)Ch06 用 Stub 进行粗粒度测试

本章将研究 JUnit5 测试依赖于外部资源的应用程序。外部资源包含: HTTP 服务器、数据库服务器 和 物理设备等。

 

几种依赖外部资源的情况处理:

  • 1. 依赖特定运行环境:建立真正的环境,但并不总是可行;
  • 2. 通过 HTTP 依赖另一个 Web 服务:模拟该服务器;
  • 3. 依赖另一部分未完成的应用程序:用一个类似行为的替代对象替换缺失的部分;

 

替代对象有2种策略:

1. stub:

  • 当编写 stub 时,一开始就有一个预先确定的行为;
  • stub 代码是在测试的外部编写的,并且总是有固定的行为。其方法通常会返回硬编码的值;
  • 使用 stub 进行测试的模式:初始化 stub > 运行测试 > 验证断言;

2. mock object:

  • 预先没有确定的行为;
  • 在运行测试时,有效使用 mock 前,应在 mock 上设置预期。可以运行不同的测试,可以重新初始化一个 mock 并对其设置不同的预期;
  • 使用 mock object 进行测试的模式:初始化 mock > 设置预期 > 运行测试 > 验证断言;

 


7.1 stub 简介

stub:一种机制。用来模拟真实代码或尚未完成的代码所产生的行为。通常不会改变所测试的代码,是适应代码以提供无缝集成。

stub 是一段代码,在运行期间使用插入的 stub 来替代真实的代码,以便将其调用者与真正的实现隔离。其目的是用一个简单行为替换一个复杂行为,从而可以测试真实代码的一部分。

 

使用到 stub 的情况:

  • 不能修改一个现有系统,因为太复杂,容易崩溃;
  • 依赖一个无法控制的环境;
  • 替换一个成熟的外部系统,如文件系统、到服务器的连接或数据库;
  • 运行粗粒度的测试,如不同子系统的集成测试;

 

使用 mock object 的情况:

  • 需要细粒度的测试来提供精确的信息,如查找失败的原因;
  • 单独测试一小部分代码;

 

stub 的缺点:

  • 编码复杂,且自身也需要调试;
  • 因为复杂,很难维护;
  • 并不适合细粒度的单元测试;
  • 每种情况都需要一种不同的 stub 策略;

 


7.3 用 stub 替换服务器资源

// WebClient 类
public class WebClient {
    public String getContent(URL url) {
        // 创建 StringBuffer 对象,存储可以递增的字符串
        StringBuffer content = new StringBuffer();

        try {
            // 使用给定的URL对象打开一个连接,并得到了一个HttpURLConnection对象。然后它进行了类型转换,将得到的连接对象转换为HttpURLConnection类型
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            // 设置一个网络连接可以读取输入流。
            // 在网络编程中,当你想要从网络连接读取数据时,你需要设置这个连接可以输入数据。
            // 在Java中,你可以使用 HttpURLConnection 或 URLConnection 类的 setDoInput(boolean doInput) 方法来
            //          设置这个连接可以输入数据。doInput 参数为 true 表示允许从连接读取数据,为 false 则表示不允许。
            // 这行代码通常在建立网络连接之前使用,以确保你可以从该连接读取数据。
            connection.setDoInput(true);
            // 通过HttpURLConnection对象调用getInputStream()方法,得到一个输入流is,这个输入流用于读取从URL获取的数据
            InputStream is = connection.getInputStream();
            // 创建了一个字节数组buffer,用于临时存储从输入流中读取的数据。
            byte[] buffer = new byte[2048];
            int count;
            // 循环持续读取输入流中的数据,直到没有数据可读(当is.read(buffer)返回-1时)。每次读取的数据量存储在变量count中。读取的数据被转换为字符串并追加到content中
            // ** 通过查看 is.read() 的源码,其返回值就是 读取的数据量
            while (-1 != (count = is.read(buffer))) {
                // String(byte[] bytes, int offset, int length)
                /**
                 * 将从输入流中读取的字节数据转换为字符串,并将其追加到StringBuffer对象content中。
                 * 具体来说,new String(buffer, 0, count)创建了一个新的字符串对象,该对象是由buffer数组中的字节数据转换而来的。
                 *         buffer数组中的字节数据从索引0开始,一直到索引count-1,因为count表示实际读取的字节数据的长度。
                 * 通过调用StringBuffer对象的append()方法,这个新的字符串被追加到content中,这样在循环结束后,content中就包含了
                 *    从输入流中读取的所有数据。
                 */
                content.append(new String(buffer, 0, count));
            }
        } catch (IOException e) {
            throw new  RuntimeException(e);
        }
        return content.toString();
    }
}

解决方案一:stub 一个 web 服务器。缺点:很复杂。

public class TestWebClient {
    private WebClient client = new WebClient();

    @BeforeAll
    // 替换 服务器环境的方式:嵌入式 Jetty 环境(创建 Server -> 设置上下文 Context -> 设置处理器 handler -> 启动服务器)
    static void setUp() throws Exception {
        // 创建了一个新的Server对象,并初始化它监听8081端口
        Server server = new Server(8081);

        // 创建了一个新的Context对象,并将其关联到服务器和一个路径(“/testGetContentOk”)
        // 在Servlet中,Context是一个对象的容器,它包含了关于Servlet引擎的配置信息
        // public Context(HandlerContainer parent, String contextPath)
        Context contentOkContext = new Context(server, "/testGetContentOk");
        // 为上一步创建的Context对象设置一个处理器。处理器用于处理来自客户端的请求。这里的处理器是TestGetContentOkHandler的一个新实例
        contentOkContext.setHandler(new TestGetContentOkHandler());

        Context contentErrorContext = new Context(server, "/testGetContentError");
        contentErrorContext.setHandler(new TestGetContentServerErrorHandler());

        Context contentNotFoundContext = new Context(server, "/testGetContentNotFound");
        contentNotFoundContext.setHandler(new TestGetContentNotFoundHandler());

        // 在JVM关闭时会发送一个关闭信号给服务器,要求其停止所有的工作。设置为true表示服务器将在JVM关闭时立即停止所有的工作
        server.setStopAtShutdown(true);
        // 启动服务器,开始监听指定的端口并等待客户端的请求
        server.start();
    }

    @AfterAll
    static void tearDown() {
        // 没有逻辑实现,是因为已经通过编程,使服务器关闭时停止。当 JVM 关闭时,服务器实例显式地停止(server.setStopAtShutdown(true))。
        // empty
    }

    @Test
    public void testGetContentOk() throws MalformedURLException {
        String workingContent = client.getContent(new URL("http://localhost:8081/testGetContentOk"));
        assertEquals("It works", workingContent);
    }

    // 该类被上面的 static setUp() 引用,所以该类被声明为 static
    private static class TestGetContentOkHandler extends AbstractHandler {

        @Override
        public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
            // 从HttpServletResponse对象中获取一个输出流,并将其赋值给变量out。这个输出流可以用来向客户端发送HTTP响应的正文内容
            OutputStream out = response.getOutputStream();
            // 创建了一个新的ByteArrayISO8859Writer对象,并将其赋值给变量writer。这个类可以用来将字符串转换为字节数组,并支持ISO 8859-1编码
            ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer();
            // 将字符串"It works"写入到上一步创建的ByteArrayISO8859Writer对象中
            writer.write("It works");
            // 清空缓冲区,确保所有数据都被写出
            writer.flush();

            // 设置HTTP响应头的CONTENT_LENGTH为writer.size(),也就是刚才写入的字符串的字节数。这告诉客户端接收到多少字节的数据
            // setIntHeader(String name, int value). name -- name of the header
            response.setIntHeader(HttpHeaders.CONTENT_LENGTH, writer.size());
            // 将writer缓冲区中的数据写入到上一步获取的输出流中,也就是发送给客户端
            writer.writeTo(out);
            // 再次调用flush()方法,以确保所有数据都被完全发送到客户端
            out.flush();
        }
    }

    /**
     * Handler to handle bad requests to the server
     */
    private static class TestGetContentServerErrorHandler extends AbstractHandler {

        @Override
        public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Handler to handle requests that request unavailable content.
     */
    private static class TestGetContentNotFoundHandler extends AbstractHandler {

        @Override
        public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

解决方案二(稍微轻量):替换 Http 连接。

思路:得益于 Java 的 URL 和 HttpURLConnection 类,我们可以插入自定义的 协议处理器 来处理任何类型的通信协议,可以使任何对 HttpURLConnection 类的调用指向自己的类,这些类会返回测试需要的任何内容。

实现:
1)创建自定义的 URL 协议处理器,需要调用 URL.setURLStreamHandlerFactory()
2) URLStreamHandlerFactory类有一个方法叫createURLStreamHandler(String protocol),该方法根据给定的协议(例如"http:", "ftp:"等)返回一个URLStreamHandler。如果工厂不能处理该协议,那么它应该返回null。
所以这里需要 一个类 自定义类 StubStreamHandlerFactory,并实现 URLStreamHandlerFactory,在其方法createURLStreamHandler() 里实现自定义的 HttpURLStreamHandler
3) StubHttpURLStreamHandler 在 openConnection() 去调用自定义的 HttpURLConnection 类 4)在自定义的 StubHttpURLConnection 类中实现 getInputStream() 返回测试需要的内容
// TestWebClient1 
public class TestWebClient1 {

    @BeforeAll
    static void setUp() {
        // URL.setURLStreamHandlerFactory() 用于为URL类设置一个URLStreamHandlerFactory。
        //     这个工厂用于创建URLStreamHandler实例,这些实例能够处理URLs的特定部分,比如文件系统路径、网络套接字路径和Mailto: URL路径。
        URL.setURLStreamHandlerFactory(new StubStreamHandlerFactory());
    }

    private static class StubStreamHandlerFactory implements URLStreamHandlerFactory {

        @Override
        // URLStreamHandlerFactory类有一个方法叫createURLStreamHandler(String protocol),该方法
        //      根据给定的协议(例如"http:", "ftp:"等)返回一个URLStreamHandler。如果工厂不能处理该协议,那么它应该返回null。
        public URLStreamHandler createURLStreamHandler(String protocol) {
            return new StubHttpURLStreamHandler();
        }
    }

    private static class StubHttpURLStreamHandler extends URLStreamHandler {

        @Override
        protected URLConnection openConnection(URL url) {
            return new StubHttpURLConnection(url);
        }
    }

    @Test
    public void testGetContentOk() throws MalformedURLException {
        WebClient client = new WebClient();
        String workingContent = client.getContent(new URL("http://localhost:/"));
        assertEquals("It works", workingContent);
    }
}
// StubHttpURLConnection 类的实现
public class StubHttpURLConnection extends HttpURLConnection {

    // isInput 是告诉我们是否使用了 URL 连接进行输入
    private boolean isInput = true;

    /**
     * Constructor for the HttpURLConnection.
     *
     * @param url the URL
     */
    protected StubHttpURLConnection(URL url) {
        super(url);
    }

    @Override
    public InputStream getInputStream() throws IOException {
        if (!isInput) {
            throw new ProtocolException("Cannot read from URLConnection" + " if doInput=false (call setDoInput(true))");
        }
        // public ByteArrayInputStream(byte buf[])
        // String().getBytes() 返回一个 byte[]
        ByteArrayInputStream readStream = new ByteArrayInputStream(new String("It works").getBytes());
        return readStream;
    }

    @Override
    public void disconnect() {
    }

    @Override
    public boolean usingProxy() {
        return false;
    }

    @Override
    public void connect() throws IOException {
    }
}

 

posted on 2023-11-10 11:35  bruce_he  阅读(25)  评论(0编辑  收藏  举报