嘲笑"阻止"用Spock调用方法?

时间:2015-11-30 18:33:31

标签: java unit-testing spock

背景

我正在学习使用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模拟然后进行方法阻止,我可以单独和确定地验证所有这些行为,每个功能方法一个。

1 个答案:

答案 0 :(得分:1)

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

  1. 该类使用提供的套接字重复调用doSomethingWithSocket
  2. 在抛出IO异常后停止
  3. 让我们说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]
    

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