【问题标题】:Mock a "blocking" method call with Spock?用 Spock 模拟“阻塞”方法调用?
【发布时间】:2016-03-04 11:56:25
【问题描述】:

背景

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

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

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,这有点令人困惑.

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

编辑

我要验证的不是阻塞本身;我想验证每个功能方法的 one 行为。我可以使用下面 David W 提供的技术在一个方法中测试许多行为,但某些行为会根据接受的连接数、是否遇到异常、是否告诉 Listener 停止接受连接而改变等...这将使单一特征方法更加复杂,并且难以排除故障和记录。

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

【问题讨论】:

  • doSomethingWithSocket() 是否在主测试线程上运行?
  • 这就是问题所在;它不是。我可以将在套接字上实际操作的方法暴露给其他作用域并直接测试它们,但是需要担心线程问题,因为它们不打算从startListening 创建的线程外部调用。如果我公开它们,它们可以在其他地方调用,这意味着我必须做更多的工作来保持它们的线程安全。
  • 所以你可以尝试我添加的第二个 sn-p 来阻止最后一个套接字调用,直到测试完成。为了提供更多帮助,我需要一个关于如何将工作分配给另一个线程的 sn-p。

标签: java unit-testing spock


【解决方案1】:

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

  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]

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

【讨论】:

  • +1 因为这比我想出的“解决方案”要好,但它有一些问题;在多种情况下Listener 可能会停止接受新的条件,我绝对不喜欢在同一规范中进行多次(可能非常不同)验证。但我不接受的主要原因是它实际上并没有回答我的问题,即“我可以模拟阻塞的交互吗?”
  • doSomethingWithSocket 方法会阻塞,直到对闭包进行评估。您可以在闭包中添加 sleep 语句。您可以在 accept 方法上使用类似的闭包。我在规范中使用它来验证在可配置期间进行的多次重试。您要验证的阻止情况如何?
  • 我已经编辑了我的问题以更好地阐明我的意图;不过,也许我没有完全理解您的答案(诚然,我对 Groovy 一点经验都没有)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多