用 Spock 模拟 "blocking" 方法调用?
Mock a "blocking" method call with Spock?
背景
我正在学习使用 Spock 进行单元测试,我遇到了一个我似乎无法理解的问题:
Note: This example is very simplified, but it gets the idea of what I'd like to achieve across.
我有一个 class(称之为 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
return 由 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()
都会 return 模拟 Socket
。由于此调用(有意)被调用的次数不定(因为我想接受不定数量的入站连接),因此模拟 Socket
上的所有交互都发生不定次数(取决于速度)机器是,需要多长时间运行,等等...)
使用基数
我可以使用交互的基数来指定 至少 一次交互,如下所示:
1.._ * socket.someMethod()
但是这让我有些不快;我并不是真的在寻找 至少 次互动,我真的在寻找 一次 次互动。
返回 null
我可以做这样的事情(return Mocked 套接字一次,然后为 null):
serverSocket.accept() >>> [socket, null]
但是我仍然有大量调用 doSomethingWithSocket
传递 null
参数,然后我必须检查并忽略(或报告)。如果我忽略它,我可能会错过报告合法问题(我不认为 ServerSocket#accept
可以 return null
,但由于 class 不是最终的,也许有人实现了他们自己的版本 can) 但如果我报告它,我的测试日志会被报告 expected 结果的日志消息污染 [= =98=]没想到一个.
使用闭包和副作用
诚然,我不是一名 Groovy 程序员,这是我第一次与 Spock 合作,如果这是 完全错误的 [=101],我深表歉意=] 要做的事情,或者如果我误解了 Spock 是如何模拟的
我试过这个:
serverSocket.accept() >> socket >> {while(true) {}; null }
在 accept
方法被调用之前就一直循环;我不是 100% 确定为什么,因为我认为在第二次调用 accept 方法之前不会评估闭包?
我也试过这个:
serverSocket.accept() >>> [socket, { while(true){}; null }]
据我了解,当第一次调用 accept
方法时,socket
将被 returned。进一步的调用将调用闭包,它会无限循环,因此应该阻塞。
在本地,这似乎有效,但是当 运行 由 CI 服务(具体来说,Travis CI)构建时,我仍然看到测试输出表明 accept
方法正在重新调整 null
,这有点令人困惑。
我只是想做一些做不到的事情吗?这对我或任何事情都不是一个交易破坏者(我可以忍受嘈杂的测试日志)但我真的很想知道这是否可能。
编辑
我要验证的不是阻塞本身;我想验证每个特征方法的 one 行为。我可以使用下面 David W 提供的技术在单个方法中测试许多行为,但是某些行为会根据接受的连接数、是否遇到异常、是否告知 Listener
停止而改变接受连接等...这会使单一特征方法变得更加复杂并且难以进行故障排除和记录。
如果我可以从 accept
方法 return 定义数量的 Socket
模拟,然后使该方法阻塞,我可以单独和确定地验证所有这些行为,一个每个特征方法。
一个单元测试应该只测试一个 class 不依赖于其他 classes。从你粘贴的代码来看,你只需要验证两件事
- class 使用提供的套接字重复调用 doSomethingWithSocket
- 抛出IO异常后停止
假设 doSomething 刚刚传递给委托 class 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++
}
这将验证您提供的示例代码的所有行和条件都经过测试。我使用委托是因为没有 class 中的其他代码,我看不到其他条件(需要不同的模拟套接字)
您可以做另一个测试来测试不同套接字的行为。
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]
如果你需要最后一个套接字来保持主线程,你可以像我上面那样让它休眠。您可以通过使方法调用相互交互来添加更复杂的行为。
背景
我正在学习使用 Spock 进行单元测试,我遇到了一个我似乎无法理解的问题:
Note: This example is very simplified, but it gets the idea of what I'd like to achieve across.
我有一个 class(称之为 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
return 由 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()
都会 return 模拟 Socket
。由于此调用(有意)被调用的次数不定(因为我想接受不定数量的入站连接),因此模拟 Socket
上的所有交互都发生不定次数(取决于速度)机器是,需要多长时间运行,等等...)
使用基数
我可以使用交互的基数来指定 至少 一次交互,如下所示:
1.._ * socket.someMethod()
但是这让我有些不快;我并不是真的在寻找 至少 次互动,我真的在寻找 一次 次互动。
返回 null
我可以做这样的事情(return Mocked 套接字一次,然后为 null):
serverSocket.accept() >>> [socket, null]
但是我仍然有大量调用 doSomethingWithSocket
传递 null
参数,然后我必须检查并忽略(或报告)。如果我忽略它,我可能会错过报告合法问题(我不认为 ServerSocket#accept
可以 return null
,但由于 class 不是最终的,也许有人实现了他们自己的版本 can) 但如果我报告它,我的测试日志会被报告 expected 结果的日志消息污染 [= =98=]没想到一个.
使用闭包和副作用
诚然,我不是一名 Groovy 程序员,这是我第一次与 Spock 合作,如果这是 完全错误的 [=101],我深表歉意=] 要做的事情,或者如果我误解了 Spock 是如何模拟的
我试过这个:
serverSocket.accept() >> socket >> {while(true) {}; null }
在 accept
方法被调用之前就一直循环;我不是 100% 确定为什么,因为我认为在第二次调用 accept 方法之前不会评估闭包?
我也试过这个:
serverSocket.accept() >>> [socket, { while(true){}; null }]
据我了解,当第一次调用 accept
方法时,socket
将被 returned。进一步的调用将调用闭包,它会无限循环,因此应该阻塞。
在本地,这似乎有效,但是当 运行 由 CI 服务(具体来说,Travis CI)构建时,我仍然看到测试输出表明 accept
方法正在重新调整 null
,这有点令人困惑。
我只是想做一些做不到的事情吗?这对我或任何事情都不是一个交易破坏者(我可以忍受嘈杂的测试日志)但我真的很想知道这是否可能。
编辑
我要验证的不是阻塞本身;我想验证每个特征方法的 one 行为。我可以使用下面 David W 提供的技术在单个方法中测试许多行为,但是某些行为会根据接受的连接数、是否遇到异常、是否告知 Listener
停止而改变接受连接等...这会使单一特征方法变得更加复杂并且难以进行故障排除和记录。
如果我可以从 accept
方法 return 定义数量的 Socket
模拟,然后使该方法阻塞,我可以单独和确定地验证所有这些行为,一个每个特征方法。
一个单元测试应该只测试一个 class 不依赖于其他 classes。从你粘贴的代码来看,你只需要验证两件事
- class 使用提供的套接字重复调用 doSomethingWithSocket
- 抛出IO异常后停止
假设 doSomething 刚刚传递给委托 class 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++
}
这将验证您提供的示例代码的所有行和条件都经过测试。我使用委托是因为没有 class 中的其他代码,我看不到其他条件(需要不同的模拟套接字)
您可以做另一个测试来测试不同套接字的行为。
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]
如果你需要最后一个套接字来保持主线程,你可以像我上面那样让它休眠。您可以通过使方法调用相互交互来添加更复杂的行为。