我遇到了一个与spock单元测试相关的奇怪的闭包问题,并想知道是否有人可以解释这一点。
如果我们想象一个dao,模型和服务如下:
interface CustomDao {
List<Integer> getIds();
Model getModelById(int id);
}
class CustomModel {
int id;
}
class CustomService {
CustomDao customDao
public List<Object> createOutputSet() {
List<Model> models = new ArrayList<Model>();
List<Integer> ids = customDao.getIds();
for (Integer id in ids) {
models.add(customDao.getModelById(id));
}
return models;
}
}
我想对CustomService.createOutputSet进行单元测试。我创建了以下规范:
class TestSpec extends Specification {
def 'crazy closures'() {
def mockDao = Mock(CustomDao)
def idSet = [9,10]
given: 'An initialized object'
def customService = new CustomService
customService.customDao = mockDao
when: 'createOutput is called'
def outputSet = customService.createOutputSet()
then: 'the following methods should be called'
1*mockDao.getIds() >> {
return idSet
}
for (int i=0; i<idSet.size(); i++) {
int id = idSet.get(i)
1*mockDao.getModelById(idSet.get(i)) >> {
def tmp = new Model()
int tmpId = id // idSet.get(i)
return tmp
}
}
and: 'each compute package is accurate'
2 == outputSet.size()
9 == outputSet.get(0).getId()
10 == outputSet.get(1).getId()
}
}
请注意,在这里我测试了两件事。首先,我用模拟初始化dao,验证是否正确调用了daos并返回正确的数据,然后我验证我得到了正确的输出(即“and:
”)。
棘手的部分是for循环,我想从模拟dao返回与method参数相关的模型。在上面的示例中,如果我使用简单的for (__ in idSet)
,则模型仅返回ID为10:outputSet.get(0).getId() == outputSet.get(1).getId() == 10
。如果我使用传统的for循环,并使用idSet.get(i)
设置模型,我会得到IndexOutOfBoundsException
。实现这项工作的唯一方法是检索局部变量(id
)中的值并使用变量进行设置,如上所述。
我知道这与groovy闭包有关,我怀疑spock在执行它们之前将模拟调用捕获到一组闭包中,这意味着模型创建取决于闭包的外部状态。我理解为什么我会得到IndexOutOfBoundsException,但我不明白为什么int id = idSet.get(i)
被闭包捕获而i
不是。
有什么区别?
注意:这不是实时代码,而是简化以展示我的挑战的关键。我不会也不会在getIds()和getModelById()上进行两次后续dao调用。
答案 0 :(得分:2)
虽然通过闭包来存根getModelById
,但闭包的参数必须与方法的参数匹配。如果您尝试以下内容,则不再需要id
内的局部变量for
。
for (int i=0; i<idSet.size(); i++) {
//int id = idSet.get(i)
mockDao.getModelById(idSet.get(i)) >> {int id ->
def tmp = new Model()
tmp.id = id // id is closure param which represents idSet.get(i)
return tmp
}
}
简化版将使用each
idSet.each {
mockDao.getModelById(it) >> {int id ->
def tmp = new Model()
tmp.id = id // id is closure param which represents idSet.get(i)
tmp
}
}
我们是否需要担心如果方法存在被调用多少次?
答案 1 :(得分:2)
从延迟执行的闭包中访问可变局部变量是Spock不常见的常见错误来源。
我不明白为什么int id = idSet.get(i)被闭包捕获而我不是。
前者在每次迭代时产生一个单独的提升变量,其值是常数。后者产生一个提升变量,其值随时间变化(并且在结果生成器执行之前)。
不是通过引入临时变量来解决问题,而是更好的解决方案(已由@dmahapatro提供)是声明int id ->
闭包参数。如果它被认为足以在不强制执行的情况下存根调用,则可以完全省略循环。另一个潜在的解决方案是热切地构建回报值:
idSet.each { id ->
def model = new Model()
model.id = id
1 * mockDao.getModelById(id) >> model
}