Модульное тестирование Spock и внутренние закрытия

Я столкнулся с довольно странной проблемой закрытия, связанной с модульным тестированием spock, и подумал, может ли кто-нибудь объяснить это.

Если мы представим себе дао, модель и сервис следующим образом:

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 своим макетом, проверяю, что dao правильно вызывается и возвращаю правильные данные, а затем проверяю, что получаю правильный результат (т.е. «and:»).

Сложная часть - это цикл for, в котором я хотел вернуть модели из mock dao, связанные с параметром метода. В приведенном выше примере, если я использую простой for (__ in idSet), модели возвращаются только с идентификатором 10: outputSet.get(0).getId() == outputSet.get(1).getId() == 10. Если я использую традиционный цикл for и устанавливаю модель с idSet.get(i), я получаю IndexOutOfBoundsException. Единственный способ выполнить эту работу - получить значение в локальной переменной (id) и установить с помощью переменной, как указано выше.

Я знаю, что это связано с классными замыканиями, и подозреваю, что spock захватывает фиктивные вызовы в набор замыканий перед их выполнением, а это означает, что создание модели зависит от внешнего состояния замыкания. Я понимаю, почему я получил исключение IndexOutOfBoundsException, но не понимаю, почему int id = idSet.get(i) захватывается закрытием, а i - нет.

В чем разница?

Примечание: это не живой код, а скорее упрощенный, чтобы продемонстрировать суть моей задачи. Я бы не стал и не буду делать два последовательных вызова dao для getIds () и getModelById ().


person chris.wood    schedule 30.06.2013    source источник
comment
Почему бы не использовать ››› для управления значениями, возвращаемыми имитацией getModelById? Похоже на более чистый способ делать то, что вы пытаетесь сделать. spock-framework.readthedocs.org/en/ последний /   -  person Tomas Lin    schedule 30.06.2013
comment
Если я чего-то не упускаю, я делаю именно это. Обратите внимание, что для getModelById я использую оператор сдвига вправо, чтобы вернуть значение, зависящее от его ввода. Таким образом я тестирую взаимодействие с дао. Для каждого идентификатора предоставляется метод, я ожидаю, что он впоследствии вызовет этот параметр и вернет связанный с ним объект. В моем системном коде я тестирую дальнейшие взаимодействия, поэтому важно, чтобы у меня было четко различимое состояние при оценке списка пакетов, возвращаемого методом.   -  person chris.wood    schedule 30.06.2013


Ответы (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
    }
}

Нужно ли нам беспокоиться о том, сколько раз вызывается метод, если он заглушен?

person dmahapatro    schedule 30.06.2013
comment
Спасибо за полезный ответ. Это имеет смысл как средство явной передачи аргументов в закрытие. Я не понимаю, почему в моем примере выше id = idSet.get (i) закрывается, а idSet.get (i) - нет. В чем разница? - person chris.wood; 01.07.2013
comment
@ chris.wood Сможет ли закрытие узнать, что i из idSet.get(i) в своей локальной области? - person dmahapatro; 01.07.2013
comment
Вот что интересно. Со spock он видит i как 2, его значение после выхода из цикла for. Это должно означать, что Спок не оценивает закрытия до тех пор, пока они не будут сохранены. Кстати, приведенный вами пример не работает (я тестировал ранее), потому что оценка происходит внутри замыкания. Я надеялся понять, почему int id = idSet.get(i) работает, а я - нет. - person chris.wood; 02.07.2013
comment
@ chris.wood Мне удалось пройти вышеуказанные тесты, используя те же примеры, которые я привел в своем ответе. Я также смог воспроизвести IndexOutOfBoundsException, напрямую используя idSet.get(i) внутри закрытия. Я использовал тест Grails 2.2.2 с плагином spock:0.7 вместо запуска тестов в проекте Groovy. Найдите его здесь. - person dmahapatro; 02.07.2013
comment
@ chris.wood Если хотите, я могу выложить для вас образец проекта в github. - person dmahapatro; 02.07.2013

Доступ к изменяемым локальным переменным из замыкания, выполнение которого отложено, является частым источником ошибок, не относящимся к 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
}
person Peter Niederwieser    schedule 04.07.2013
comment
Это точно ответило на мой вопрос. Спасибо. Передаю благодарность @dmahapatro за предоставление работающей реализации, хотя в вашем ответе объясняется возникшая у меня путаница с закрытием. Спасибо! - person chris.wood; 05.07.2013