简体   繁体   中英

Mock a "blocking" method call with Spock?

Background

I'm learning to use Spock for unit testing and I've come across an issue I can't seem to get my head around:

Note: This example is very simplified, but it gets the idea of what I'd like to achieve across.

I have a class (call it Listener ) which accepts a java.net.ServerSocket as a constructor parameter; it has a startListening method, which spawns a new thread which does the following (heavily reduced for brevity):

while(listening) {
    try {
        Socket socket = serverSocket.accept();
        doSomethingWithSocket(socket);
    } catch(IOException ex) {
        ex.printStackTrace();
        listening = false;
    }
}

In normal operation, the serverSocket.accept() call blocks until a connection is made to the ServerSocket .

Problem

I'd like to be able to test the interactions on the Socket returned by serverSocket.accept() . I can do this with Spock in the following way:

given: "A ServerSocket, Socket, and Listener"
    def serverSocket = Mock(ServerSocket)
    def socket = Mock(Socket)
    serverSocket.accept() >> socket
    def listener = new Listener(serverSocket)

when: "Listener begins listening"
    listener.startListening()

then: "Something should be done with the socket"
    // Verify some behavior on socket

At first glance, this works fine, except that every call to serverSocket.accept() will return the mocked Socket . Since this call is (intentionally) being invoked an indefinite number of times (because I want to accept an indefinite number of inbound connections) all of the interactions on the mock Socket occur an indefinite number of times (depending on how fast the machine is, how long it takes to run, etc...)

Using cardinality

I could use the cardinality of the interaction to specify at least one interaction, like so:

1.._ * socket.someMethod()

But something about that rubs me the wrong way; I'm not really looking for at least one interaction, I'm really looking for one interaction.

Returning null

I could do something like this (to return the Mocked socket once and then null):

serverSocket.accept() >>> [socket, null]

But then I still have tons of calls to doSomethingWithSocket that pass a null parameter, which I then have to check for and ignore (or report). If I ignore it, I might miss reporting a legitimate issue (I don't think ServerSocket#accept can ever return null , but since the class isn't final maybe someone implements their own version which can ) but if I report it, my test logs get polluted with the log message that's reporting an expected outcome as an unexpected one.

Using closures and side-effects

I'm admittedly not a Groovy programmer by trade and this is the first time I've worked with Spock, so my apologies if this is a just plain wrong thing to do or if I'm misunderstanding something about how Spock does mocking

I tried this:

serverSocket.accept() >> socket >> {while(true) {}; null }

This loops forever before the accept method is even called; I'm not 100% sure why, as I didn't think the closure would be evaluated until the accept method was called a second time?

I also tried this:

serverSocket.accept() >>> [socket, { while(true){}; null }]

It's my understanding that when the accept method is called the first time, socket will be returned. Further calls will invoke the closure, which loops infinitely and should therefore block.

Locally, this appears to work, but when the build is run by a CI service (specifically, Travis CI) I still see test output indicating that the accept method is retuning null , which is a little bit confusing.

Am I just trying to do something that can't be done? This isn't a deal breaker for me or anything (I can live with noisy test logs) but I'd really like to know if this is possible.

Edit

It's not the blocking itself I'm attempting to verify; I'd like to verify one behavior per feature method. I could use the technique provided by David W below to test many behaviors in a single method, but some behaviors change depending on the number of accepted connections, whether an exception has been encountered, whether the Listener has been told to stop accepting connections, etc... which would make that single feature method much more complicated and difficult to troubleshoot and document.

If I can return a defined number of Socket mocks from the accept method and then make the method block, I can verify all of these behaviors individually and deterministically, one per feature method.

A unit test should only test one class no dependencies on other classes. From the code you pasted, you only need to verify two things

  1. The class repeatedly calls doSomethingWithSocket with the provided socket
  2. It stops after an IO exception is thrown

Let's say the doSomething is just passed to a delegate class DoSomethingDelegate

setup:
def delegate = Mock(DoSomethingDelegate)
List actualExceptions = [null,null,new IOException()]
int index = 0
// other setup as in your question

when:
new Listener(serverSocket).startListening()

then:
noExceptionThrown()
3 * delegate.doSomethingWithSocket(socket) {
  if(actualExceptions[index]) {
    throw actualExceptions[index]
  }
  index++
}

This will verify that all lines and conditions of the sample code you provided are tested. I used the delegate because without the other code in the class, I can't see what other conditions (different mock sockets are required)

You could do another test to test the behavior different sockets.

setup:
List sockets = []
sockets << Mock(Socket)
sockets << Mock(Socket)
// repeat as needed
socket[3].isClosed() >> {Thread.sleep(1000);false}
serverSocket.accept() >> {sockets[socketNumber]}
// add different mock behavior
// when block
// then
where:
socketNumber << [1,2,3]

If you need a last socket to hold the main thread, you can make it sleep as I have done above. You can add more complex behavior by making the method calls interact with each other.

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