与 Spock 模拟"blocking"方法调用?



背景

我正在学习使用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

问题

我希望能够测试serverSocket.accept()返回的Socket上的交互。我可以通过以下方式使用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套接字,然后为空):

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

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

使用闭包和副作用

无可否认,我不是Groovy程序员,这是我第一次与Spock合作,所以如果这是一件完全错误的事情,或者我对Spock如何嘲笑有误解,我深表歉意

我试过这个:

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

在调用accept方法之前,这将永远循环;我不能100%确定为什么,因为我认为在第二次调用accept方法之前不会评估闭包?

尝试过这个:

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

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

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

我只是想做一些不能做的事吗?这对我或任何事情来说都不是一个破坏交易的因素(我可以忍受嘈杂的测试日志),但我真的很想知道这是否可能。

编辑

我试图验证的不是阻塞本身;我想验证每个特征方法的一个行为。我可以使用下面David W提供的技术在一个方法中测试许多行为,但有些行为会随着接受连接的数量、是否遇到异常、Listener是否被告知停止接受连接等而变化……这将使该单一功能方法更加复杂,难以进行故障排除和记录。

如果我可以从accept方法返回定义数量的Socket mock,然后使方法块,我就可以单独地、决定性地验证所有这些行为,每个特征方法一个。

单元测试应该只测试一个类,而不测试对其他类的依赖关系。从你粘贴的代码中,你只需要验证两件事

  1. 该类使用提供的套接字重复调用doSomethingWithSocket
  2. 在引发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]

如果你需要最后一个插座来固定主线程,你可以像我上面做的那样让它休眠。您可以通过使方法调用相互交互来添加更复杂的行为。

最新更新