2

我遇到了一个与 spock 单元测试相关的非常奇怪的关闭问题,想知道是否有人可以解释这一点。

如果我们想象一个 dao、model 和 service 如下:

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()

}
}

请注意,在这里我测试了两件事。首先,我用我的 mock 初始化 dao,验证 daos 被正确调用并返回正确的数据,然后我验证我得到了正确的输出(即“ and:”)。

棘手的部分是 for 循环,我想在其中从模拟 dao 返回与方法参数相关的模型。在上面的例子中,如果我使用一个简单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 调用。

4

2 回答 2

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
    }
}

如果方法被存根,我们是否需要担心方法被调用多少次?

于 2013-06-30T03:54:41.680 回答
2

从延迟执行的闭包访问可变局部变量是不特定于 Spock 的常见错误来源。

我不明白为什么 int id = idSet.get(i) 被闭包捕获而 i 不是。

前者每次迭代都会产生一个单独的提升变量,其值是恒定的。后者产生单个提升变量,其值随时间变化(并且在结果生成器执行之前)。

不是通过引入临时变量来解决问题,更好的解决方案(已经由@dmahapatro 给出)是声明一个int id ->闭包参数。如果认为在不强制执行调用的情况下存根调用足够好,则可以完全省略循环。另一个潜在的解决方案是急切地构造返回值:

idSet.each { id ->
    def model = new Model()
    model.id = id
    1 * mockDao.getModelById(id) >> model
}
于 2013-07-04T19:53:00.243 回答