简体   繁体   English

用 Spock 模拟“阻塞”方法调用?

[英]Mock a "blocking" method call with Spock?

Background背景

I'm learning to use Spock for unit testing and I've come across an issue I can't seem to get my head around:我正在学习使用Spock进行单元测试,但遇到了一个我似乎无法理解的问题:

Note: This example is very simplified, but it gets the idea of what I'd like to achieve across.注意:这个例子非常简化,但它了解我想要实现的目标。

I have a class (call it Listener ) which accepts a java.net.ServerSocket as a constructor parameter;我有一个类(称为Listener ),它接受java.net.ServerSocket作为构造函数参数; it has a startListening method, which spawns a new thread which does the following (heavily reduced for brevity):它有一个startListening方法,它产生一个新线程,它执行以下操作(为简洁起见,大量减少):

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

In normal operation, the serverSocket.accept() call blocks until a connection is made to the ServerSocket .在正常操作中, serverSocket.accept()调用会阻塞,直到与ServerSocket建立连接。

Problem问题

I'd like to be able to test the interactions on the Socket returned by serverSocket.accept() .我希望能够测试serverSocket.accept()返回的Socket上的交互。 I can do this with Spock in the following way:我可以通过以下方式使用 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

At first glance, this works fine, except that every call to serverSocket.accept() will return the mocked Socket .乍一看,这很好用,只是每次调用serverSocket.accept()都会返回serverSocket.accept()Socket Since this call is (intentionally) being invoked an indefinite number of times (because I want to accept an indefinite number of inbound connections) all of the interactions on the mock Socket occur an indefinite number of times (depending on how fast the machine is, how long it takes to run, etc...)由于此调用(有意)被无限次调用(因为我想接受无限数量的入站连接),因此模拟Socket上的所有交互都发生了无限次(取决于机器的速度,运行需要多长时间,等等...)

Using cardinality使用基数

I could use the cardinality of the interaction to specify at least one interaction, like so:我可以使用交互的基数来指定至少一个交互,如下所示:

1.._ * socket.someMethod()

But something about that rubs me the wrong way;但是这件事让我觉得不妥; I'm not really looking for at least one interaction, I'm really looking for one interaction.我不是真的在寻找至少一种互动,我真的在寻找一种互动。

Returning null返回null

I could do something like this (to return the Mocked socket once and then null):我可以做这样的事情(返回模拟套接字一次然后为空):

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

But then I still have tons of calls to doSomethingWithSocket that pass a null parameter, which I then have to check for and ignore (or report).但是后来我仍然有大量对doSomethingWithSocket的调用传递了一个null参数,然后我必须检查并忽略(或报告)。 If I ignore it, I might miss reporting a legitimate issue (I don't think ServerSocket#accept can ever return null , but since the class isn't final maybe someone implements their own version which can ) but if I report it, my test logs get polluted with the log message that's reporting an expected outcome as an unexpected one.如果我忽略它,我可能会错过报告一个合法的问题(我不认为ServerSocket#accept可以返回null ,但由于该类不是最终的,也许有人实现了他们自己的版本可以)但是如果我报告它,我的测试日志被将预期结果报告为意外结果的日志消息所污染。

Using closures and side-effects使用闭包和副作用

I'm admittedly not a Groovy programmer by trade and this is the first time I've worked with Spock, so my apologies if this is a just plain wrong thing to do or if I'm misunderstanding something about how Spock does mocking诚然,我不是一个 Groovy 程序员,这是我第一次与 Spock 合作,所以如果这是一个完全错误的做法,或者我误解了 Spock 是如何嘲笑的,我深表歉意

I tried this:我试过这个:

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

This loops forever before the accept method is even called;在调用accept方法之前,这将永远循环; I'm not 100% sure why, as I didn't think the closure would be evaluated until the accept method was called a second time?我不是 100% 确定为什么,因为我认为在第二次调用 accept 方法之前不会评估闭包?

I also tried this:试过这个:

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

It's my understanding that when the accept method is called the first time, socket will be returned.我的理解是当第一次调用accept方法时,会返回socket Further calls will invoke the closure, which loops infinitely and should therefore block.进一步的调用将调用闭包,它无限循环,因此应该阻塞。

Locally, this appears to work, but when the build is run by a CI service (specifically, Travis CI) I still see test output indicating that the accept method is retuning null , which is a little bit confusing.在本地,这似乎有效,但是当构建由 CI 服务(特别是 Travis CI)运行时,我仍然看到测试输出表明accept方法正在重新调整null ,这有点令人困惑。

Am I just trying to do something that can't be done?我只是想做一些无法完成的事情吗? This isn't a deal breaker for me or anything (I can live with noisy test logs) but I'd really like to know if this is possible.这对我或任何事情都不是一个交易破坏者(我可以忍受嘈杂的测试日志),但我真的很想知道这是否可能。

Edit编辑

It's not the blocking itself I'm attempting to verify;我试图验证的不是阻塞本身; I'd like to verify one behavior per feature method.我想验证每个功能方法的一种行为。 I could use the technique provided by David W below to test many behaviors in a single method, but some behaviors change depending on the number of accepted connections, whether an exception has been encountered, whether the Listener has been told to stop accepting connections, etc... which would make that single feature method much more complicated and difficult to troubleshoot and document.我可以使用下面 David W 提供的技术在单个方法中测试许多行为,但某些行为会根据接受的连接数、是否遇到异常、是否已告知Listener停止接受连接等而改变...这将使该单一特征方法更加复杂且难以进行故障排除和记录。

If I can return a defined number of Socket mocks from the accept method and then make the method block, I can verify all of these behaviors individually and deterministically, one per feature method.如果我可以从accept方法返回定义数量的Socket模拟,然后使方法块,我可以单独和确定性地验证所有这些行为,每个功能方法一个。

A unit test should only test one class no dependencies on other classes.单元测试应该只测试一个类,而不依赖于其他类。 From the code you pasted, you only need to verify two things从你粘贴的代码来看,你只需要验证两件事

  1. The class repeatedly calls doSomethingWithSocket with the provided socket该类使用提供的套接字重复调用 doSomethingWithSocket
  2. It stops after an IO exception is thrown抛出 IO 异常后停止

Let's say the doSomething is just passed to a delegate class DoSomethingDelegate假设 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++
}

This will verify that all lines and conditions of the sample code you provided are tested.这将验证您提供的示例代码的所有行和条件都经过测试。 I used the delegate because without the other code in the class, I can't see what other conditions (different mock sockets are required)我使用委托是因为没有类中的其他代码,我看不到其他条件(需要不同的模拟套接字)

You could do another test to test the behavior different sockets.您可以进行另一项测试来测试不同套接字的行为。

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]

If you need a last socket to hold the main thread, you can make it sleep as I have done above.如果你需要一个最后一个套接字来保存主线程,你可以像我上面所做的那样让它休眠。 You can add more complex behavior by making the method calls interact with each other.您可以通过使方法调用相互交互来添加更复杂的行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM