java - 用 Spock 模拟 "blocking"方法调用?

标签 java unit-testing spock

背景

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

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



我有一个接受 Listener 的类(class)(称之为 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
我可以做这样的事情(返回一次模拟套接字然后返回 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 ,这有点令人困惑。

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

编辑

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

如果我可以返回定义数量的 Socket来自 accept 的模拟方法,然后使方法 block ,我可以单独和确定性地验证所有这些行为,每个特征方法一个。

最佳答案

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

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

  • 假设 doSomething 只是传递给委托(delegate)类 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++
    }
    

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

    你可以做另一个测试来测试不同套接字的行为。
    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]
    

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

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

    相关文章:

    java - android 获取图片按时间排序

    c# - 为什么 Controller.UpdateModel 不使用单元测试中的全局化验证属性验证模型?

    unit-testing - 来自 golang 包的模拟方法

    java - Minecraft Coders 在 Eclipse 中运行时出现包错误

    java - 模糊和聚焦紧密耦合?

    android - 单元测试 android

    unit-testing - Spock 未检测到方法调用

    hibernate - Grails单元测试失败:此实现GORM当前不支持基于字符串的查询,例如[executeQuery]

    grails - Windows 7 命令提示符使用 geb 和 spock 运行 grails 功能测试

    java - 使用递归方法获取因子