【问题标题】:Is it possible to stub or mock a SocketChannel with Spock?是否可以使用 Spock 存根或模拟 SocketChannel?
【发布时间】:2016-12-16 13:20:16
【问题描述】:

基本上我有一个 Java 类,它在套接字通道上执行选择,我想存根通道以便我可以测试选择是否按预期工作。

例如,被测试的类大致是这样做的:

class TestedClass {
    TestedClass(SocketChannel socket) { this.socket = socket }

    // ...
    SocketChannel socket;
    // ...
    void foo() {
        // Wait a while for far end to close as it will report an error 
        // if we do it.  But don't wait forever! 
        // A -1 read means the socket was closed by the other end.
        // If we select on a read, we can check that, or time out 
        // after a reasonable delay.

        Selector selector = Selector.open();
        socket.configureBlocking(false);
        socket.register(selector, SelectionKey.OP_READ);
        while(selector.select(1000) == 0) {
            Log.debug("waiting for far end to close socket...")
        }

        ByteBuffer buffer = ByteBuffer.allocate(1);
        if (socket.read(buffer) >= 0) {
            Log.debug("far end didn't close");
            // The far end didn't close the connection, as we hoped
            abort(AbortCause.ServerClosed);
        }

        Log.debug("far end closed");
    }
}

我希望能够测试这样的东西:

def "test we don't shut prematurely" () {
    when:
    boolean wasClosedPrematurely
    SocketChannel socket = Stub(@SocketChannel) {
        // insert stub methods here ....
    }

    TestedClass tc = new TestedClass(socket)
    tc.foo();

    then:
    wasClosedPrematurely == false
}

这是基于一个真实的例子,但细节并不重要。总体目标是如何对支持选择的 SocketChannels 进行存根,这样我就不必创建一个真实的客户端来进行测试。

我也知道它比仅存根 SocketChannel 更复杂:似乎我需要拦截 Selector.open() 或以某种方式提供自定义系统默认 SelectorProvider。如果我只是简单地存根 SocketChannel,当我尝试将通过 Selection.open() 获得的选择器注册到我的存根时,我会得到一个 IllegalSelectorException,不幸的是,基本的 AbstractSelectableChannel#register 方法是最终的。

但是我找不到任何有用的指示来说明 Spock Mocks 如何或是否可以实现这一点,而且这似乎是一件很常见的事情,所以在这里提出一个很好的问题。有人可以帮忙吗?

【问题讨论】:

    标签: java unit-testing spock socketchannel


    【解决方案1】:

    Spock 使用CGLIB 模拟/存根/间谍类。 CGLIB 不能覆盖 final 方法。 SocketChannel 有很多 final 方法(例如 configureBlocking),但是 CGLIB 不会失败,而是使用原始方法。由于 configureBlocking 是最终的,它会在您的测试中使用。

    public final SelectableChannel configureBlocking(boolean block) throws IOException { synchronized (regLock) { if (!isOpen()) throw new ClosedChannelException(); if (blocking == block) return this; if (block && haveValidKeys()) throw new IllegalBlockingModeException(); implConfigureBlocking(block); blocking = block; } return this; }

    所以 configureBlocking 需要初始化 regLock 变量,但是当你为这个类创建存根时,变量没有初始化,你会在这里得到 NPE。

    问题是如何处理它? 好吧,我想说,首先,尝试使用接口而不是类。 如果不可能,请尽量不要调用 final 方法。 如果仍然不可能,您必须查看班级内部并找出应该嘲笑的内容。 我看到的最后一个选项是进行完整的集成测试:创建两个套接字并连接它们。

    【讨论】:

    • 使用通配符方法 _ >> { new Exception().printStackTrace() } 对 SocketChannel 进行存根显示同时调用了 configureBlocking 和 implConfigureBlocking;出于某种原因,为后者添加存根方法对我不起作用。但这不是显示停止器 - 它是调用 #register 方法的时候。这也是最终的,内部(Java 7)检查套接字提供程序是否实现了内部私有接口,如果没有,则抛出 IllegalSelectorException,
    • 我认为问题出在 SelectorImpl::register 第 117 行。 if(!(var1 instanceof SelChImpl)) { throw new IllegalSelectorException(); } else { ...坦率地说,你不能拥有抽象类(SocketChannel)的实例,但CGLIB让你拥有一个。尝试使用 SocketChannelImpl 而不是 SocketChannel 作为一个肮脏的黑客,但我会审查整个测试,考虑它的类型(单元,接受,集成),它的作用并重写它而不使用像存根抽象类这样的黑客和技巧。
    • 确切地说:要么我必须使用整个内部实现来获取 SelChImpl 实例,要么我必须以某种方式诱导 Selection.open() 返回我自己的一个,这不会强制执行此检查.我希望后一种解决方案可能有一些现有技术,因为它似乎需要重新实现一堆相互关联的类型。无论哪种方式,NIO 的设计似乎都阻碍了简单的单元测试!网络上对此几乎没有评论。
    • 我想问你到底想测试什么?
    • 嗯,最初我只是想为从旧 IO 重构为 NIO 的代码维护一些测试。 Socket / Stream 对于存根来说相对简单,它们也是如此。这些测试是针对子系统而不是单个类的,因此不是严格的单类单元测试,但它们旨在测试服务器端错误案例,这些案例不容易通过存根客户端直接控制或访问一个真正的套接字,例如超时,以及来自套接字的特定返回码。您可以将它们视为回归测试。
    【解决方案2】:

    我想我可能已经找到了自己问题的答案。

    所以Selector.open() 不能直接被拦截——但它只是调用SocketProvider.provider().openSelector(),而SocketProvider.provider()SocketProvider.provider 字段的惰性静态访问器。 (至少在我的情况下,Java 7)

    因此,我们可以简单地设置这个provider 字段,即使它是私有的,因为Groovy 可以忽略正常的Java 可见性限制。一旦设置为我们自己的存根实例,以后所有的Selector.open() 调用都将使用它(需要注意的是这是一个全局更改,可能会影响其他未测试的代码)。

    详细信息取决于您当时想要做什么,但如下所示,您可以返回其他类的存根,例如 AbstractSelectableChannel。

    工作示例如下。

    class SocketStubSpec extends Specification {
    
        SocketChannel makeSocketChannel(List events) {
            // Insert our stub SelectorProvider which stubs everything else
            // required, and records what happened in the events list.
            SelectorProvider.provider = Stub(SelectorProvider) {
                openSelector() >> {
                    Map<SelectionKey, AbstractSelectableChannel> keys = [:]
    
                    return Stub(AbstractSelector) {
                        register(_,_,_) >> { AbstractSelectableChannel c, int ops, Object att ->
                            events << "register($c, $ops, $att)"
                            SelectionKey key = Stub(SelectionKey) {
                                readyOps() >> { events << "readyOps()"; ops }
                                _ >> { throw new Exception() }
                            }
                            keys[key] = c
                            return key
                        }
                        select() >> {
                            events << "select()"
                            return keys.size()
                        }
                        selectedKeys() >> { keys.keySet() }
                        _ >> { throw new Exception() }
                    }
                }
                _ >> { throw new Exception() }
            }
    
            return Stub(SocketChannel) {
                implConfigureBlocking(_ as Boolean) >> {  boolean state -> events << "implConfigureBlocking($state)" }
                _ >> { throw new Exception() }
            }
        }
    
        def "example of SocketChannel stub with Selector" () {
            given:
            List events = []
    
            // Create a stub socket
            SocketChannel channel = makeSocketChannel(events)
    
            Selector selector = Selector.open()
            channel.configureBlocking(false);
            SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
    
            expect:
            selector.select() == 1 // our implementation doesn't block
            List keys = selector.selectedKeys().asList()
    
            keys == [key] // we have the right key
            key.isReadable() // key is readable, etc.
    
            // Things happened in the right order
            events == [
                "implConfigureBlocking(false)",
                "register(Mock for type 'SocketChannel', 1, null)",
                "select()",
                "readyOps()",
            ]
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2015-05-16
      • 1970-01-01
      • 1970-01-01
      • 2020-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-10
      • 2023-04-01
      相关资源
      最近更新 更多