背景

我正在学习使用Spock进行单元测试,遇到一个我似乎无法理解的问题:


  注意:这个示例非常简化,但是可以理解我想要实现的目标。


我有一个类(称为Listener),它接受java.net.ServerSocket作为构造函数参数。它具有一个startListening方法,该方法产生一个新线程,该线程执行以下操作(为简便起见,大量减少了该操作):

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


在正常操作中,serverSocket.accept()调用将阻塞,直到与ServerSocket建立连接为止。

问题

我希望能够测试Socket返回的serverSocket.accept()上的交互。我可以通过以下方式使用Spock进行此操作:

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


乍一看,这很好用,只是每次对serverSocket.accept()的调用都将返回模拟的Socket。由于此调用被(有意地)调用了无限次(因为我想接受无限次的入站连接),因此模拟Socket上的所有交互都发生无限次(取决于计算机的速度)是,要花多长时间,等等...)

使用基数

我可以使用交互的基数来指定至少一个交互,如下所示:

1.._ * socket.someMethod()


但是那件事使我误解了。我并不是真正在寻找至少一种互动,我真的是在寻找一种互动。

返回null

我可以做这样的事情(一次返回Mocked套接字,然后返回null):

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


但是然后我仍然有大量的doSomethingWithSocket调用传递了null参数,然后我必须检查并忽略(或报告)。如果我忽略它,我可能会错过报告合法问题的机会(我不认为ServerSocket#accept不能返回null,但是由于该类不是最终的,也许有人会实现自己的版本,但可以),但是如果我报告此问题,我的测试日志被报告预期结果为意外结果的日志消息所污染。

使用闭包和副作用

我当然不是行业的Groovy程序员,这是我第一次与Spock合作,所以我很抱歉,如果这是一件很简单的错误的事情,或者我对Spock的嘲弄方式有所误解

我尝试了这个:

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


甚至在accept方法被调用之前,它就会永远循环。我不确定为什么会这样,因为我不认为直到第二次调用accept方法时才会对闭包进行评估?

我也试过这个:

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


据我了解,第一次调用accept方法时,将返回socket。进一步的调用将调用闭包,闭包将无限循环,因此应阻塞。

在本地,这似乎可行,但是当构建由CI服务(特别是Travis CI)运行时,我仍然看到测试输出,指示accept方法正在重新调整null,这有点令人困惑。

我只是在尝试做一些无法完成的事情吗?这对我或任何事情而言都不算是破坏交易的东西(我可以忍受嘈杂的测试日志),但我真的很想知道这是否可行。

编辑

我尝试验证的不是阻塞本身,而是阻塞。我想验证每种功能方法的一种行为。我可以使用下面的David W提供的技术在一个方法中测试许多行为,但是某些行为会根据接受的连接数,是否遇到异常,是否已告知Listener停止接受连接而发生变化。等),这会使单一功能方法变得更加复杂,并且难以进行故障排除和记录。

如果我可以从Socket方法返回定义数量的accept模拟,然后使该方法成为块,则可以逐个确定性地验证所有这些行为,每个功能方法一个。

最佳答案

单元测试只能测试一个类,而不能依赖于其他类。从粘贴的代码中,您只需验证两件事


该类使用提供的套接字重复调用doSomethingWithSocket
抛出IO异常后停止


假设doSomething刚刚传递给委托类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++
}


这将验证对您提供的示例代码的所有行和条件都进行了测试。我使用委托是因为没有类中的其他代码,我看不到其他条件(需要不同的模拟套接字)

您可以做另一个测试来测试不同套接字的行为。

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]


如果您需要最后一个套接字来容纳主线程,则可以像上面一样使它休眠。您可以通过使方法调用相互交互来添加更复杂的行为。

关于java - 用Spock模拟“阻止”方法调用?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/34005525/

10-11 23:02
查看更多