简体   繁体   中英

Is it possible to stub or mock a SocketChannel with Spock?

Essentially I have a Java class which performs a select on a socket channel, and I want to stub the channel such that I can test select works as expected.

For example, this is roughly what the class being tested does:

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");
    }
}

I'd like to be able to test something like this:

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
}

This based on a real example, but the details aren't important. The general aim is how to stub SocketChannels which support selects, so that I don't have to create a real client to test against.

I also know it's more complicated than just stubbing SocketChannel: it seems like I need to intercept Selector.open() or somehow provide a custom system default SelectorProvider. If I simply stub SocketChannel I get an IllegalSelectorException when I try to register the selector obtained via Selection.open() with my stub, and the base AbstractSelectableChannel#register method is unfortunately final.

But I cannot find any useful pointers on how or whether this is even possible with Spock Mocks, and it seems like it might be quite a common thing to want, so a good question to ask here. Can anyone help?

Spock uses CGLIB to mock/stub/spy classes. CGLIB cannot override final methods. SocketChannel has a lot of final methods (eg configureBlocking), however, CGLIB doesn't fail but uses original methods. As configureBlocking is final it is used in your test.

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; }

So configureBlocking requires having the regLock variable initialized, but as you make stub for this class the variable is not initialized and you get NPE here.

The question is what to do with it? Well, I'd say, first of all, try to use interfaces but not classes. If it's not possible try not to invoke final methods. If it's still impossible you have to look inside the class and figure out what should be mocked. The last option I see is to make a full integration test: creaeting two sockets and connecting them.

I think I may have discovered an answer to my own question.

So Selector.open() can't directly be intercepted - but it simply calls SocketProvider.provider().openSelector() , and SocketProvider.provider() is a lazy static accessor for the SocketProvider.provider field. (At least in my case, Java 7)

Therefore, we can simply set this provider field, even though it is private, because Groovy can ignore normal Java visibility restrictions. Once set to our own stub instance, all future Selector.open() calls will use it henceforth (with the obvious caveat that this is a global change which could affect other code not under test).

The details depend on what you want to do then, but as below you can return stubs of the other classes such as AbstractSelectableChannel.

Working example follows.

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()",
        ]
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM