Спок - издеваться над методом закрытия Groovy?

Вот что я хочу сделать:

def mockSubdirs = []
mockSubdirs << Mock( File ){
    getName() >> 'some subdir'
    lastModified() >> 2000
}
...

File mockParentDir = Mock( File ){
    getName() >> 'parent dir'
    eachDir() >> mockSubdirs.iterator() // ??? NB eachDir is a GDK method
    // I tried things along these lines:
    // listFiles() >> mockSubdirs
    // iterator() >> mockSubdirs.iterator()
}

cut.myDirectory = mockParentDir

Код приложения такой:

def dirNames = []
myDirectory.eachDir{ 
    dirNames << it.name
}

Все вышеперечисленное дает FileNotFoundException в строке myDirectory.eachDir{...

позже

Спасибо всем 3 ответчикам за возможные решения этой проблемы. Пример кода Kriegaex мне не подходит, и я не знаю, почему. Однако его предложение взглянуть на исходный код Groovy великолепно. Итак, в NioGroovyMethods.java я обнаружил, что eachDir вызывает eachFile, что выглядит так:

public static void eachFile(final Path self, final FileType fileType, @ClosureParams(value = SimpleType.class, options = "java.nio.file.Path") final Closure closure) throws IOException {
        //throws FileNotFoundException, IllegalArgumentException {
    checkDir(self);

    // TODO GroovyDoc doesn't parse this file as our java.g doesn't handle this JDK7 syntax
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(self)) {
        for (Path path : stream) {
            if (fileType == FileType.ANY ||
                    (fileType != FileType.FILES && Files.isDirectory(path)) ||
                    (fileType != FileType.DIRECTORIES && Files.isRegularFile(path))) {
                closure.call(path);
            }
        }
    }
}

... поэтому моей первой мыслью было попытаться издеваться над Files.newDirectoryStream. Files равно final, поэтому вам нужно использовать GroovyMock, а поскольку метод static, вам придется использовать что-то вроде этого:

GroovyMock( Files, global: true )
Files.newDirectoryStream(_) >> Mock( DirectoryStream ){
    iterator() >> mockPaths.iterator()
}

... попытки в этом направлении, похоже, не работают ... Я бы совсем не удивился, если бы услышал от кого-то, что класс Files в языковом механизме Groovy не будет затронут этой попыткой имитации...

Затем я подумал, что, предположительно, toPath должен вызываться для рассматриваемого File, поэтому попробовал следующее:

File mockParentDir = Mock( File ){
    toPath() >> {
        println "toPath called"
        Mock( Path )
    }
}

... эта строка не печатается. Хорошо, я немного в тупике: чтобы получить Path из File, я даю ему, что механизм Groovy должен использовать что-то скрытое: может быть, что-то вроде getAbsolutePath()... а затем создавать Path из полученного String? Это потребует дополнительного изучения исходного кода... но если это так, вы не сможете заставить Groovy использовать фиктивный Path! или... может здесь в игру вступают другие загадочные штучки Groovy: метакласс и т.д.?


person mike rodent    schedule 18.04.2018    source источник


Ответы (3)


Это зависит от того, что вы действительно пытаетесь проверить. Вот один пример, который может быть полезен:

class DirectoryNameHelper {

    /*
     * This is silly, but facilitates answering a question about mocking eachDir
     */
    List<String> getUpperCaseDirectoryNames(File dir) {
        List<String> names = []
        dir.eachDir {File f ->
            names << f.name.toUpperCase()
        }
        names
    }
}

Тест, который издевается над eachDir. Это действительно просто проверяет, что тестируемый метод вызывает eachDir и передает закрытие, которое возвращает версию имени каждого каталога в верхнем регистре.

import groovy.mock.interceptor.MockFor
import spock.lang.Specification

class EachDirMockSpec extends Specification {

    void 'test mocking eachDir'() {
        setup:
        def mockDirectory = new MockFor(File)
        mockDirectory.demand.eachDir { Closure c ->
                File mockFile = Mock() {
                    getName() >> 'fileOne'
                }
                c(mockFile)

                mockFile = Mock() {
                    getName() >> 'fileTwo'
                }
                c(mockFile)
        }

        when:
        def helper = new DirectoryNameHelper()
        def results
        mockDirectory.use {
            def f = new File('')
            results = helper.getUpperCaseDirectoryNames(f)
        }

        then:
        results == ['FILEONE', 'FILETWO']
    }
}
person Jeff Scott Brown    schedule 18.04.2018
comment
Это очень интересно... спрос... MockFor... Мне придется вернуться к источникам Спока и немного поработать, чтобы понять это! Итак, что вы скажете на утверждение Шимона Степняка. Вы не можете издеваться над каждым Dir таким образом, потому что этот метод ... добавляется динамически? Что касается того, что я пытаюсь проверить: смоделируйте метод File.lastModified: код не показывает его, но каталоги должны быть отсортированы по дате их последнего изменения (фактически long): следовательно, я не могу использовать настоящие (временные) каталоги, Итак, как мне перебрать список фиктивных каталогов, используя eachDir? - person mike rodent; 19.04.2018

Сначала я хочу поблагодарить обоих Шаймона Степняка и Джефф Скотт Браун за их соответствующие ответы, которые были действительно проницательными и за которые я проголосовал по этой причине. Я предлагаю, чтобы ОП принял тот из них, который ему больше нравится, не этот, потому что здесь я просто объединяю оба подхода в одну спецификацию, используя один и тот же тестируемый класс и сопоставимое именование переменных в функции. методы. Я также упростил фиктивное использование подкаталогов, чтобы использовать только один фиктивный объект, который возвращает два разных имени файла при последующих вызовах через getName() >>> ['subDir1', 'subDir2'].

Итак, теперь мы можем более легко сравнить оба подхода, которые в основном делают это:

  • Подход Szymon заключается в том, чтобы полагаться на встроенные средства Spock, и это то, что следует использовать при тестировании классов Java. OTOH, здесь мы имеем дело с eachDir, специфичной для Groovy вещью. Недостатком здесь является то, что для того, чтобы реализовать этот вид насмешек, нам действительно нужно посмотреть исходный код для eachDir и одного из его вспомогательных методов, чтобы выяснить, что именно нужно заглушить, чтобы все это работало правильно. . Тем не менее, это простое и правильное решение IMO.
  • Подход Джеффа смешивает насмешки Спока с собственным MockFor Groovy, из-за чего мне было немного сложнее читать, когда я впервые столкнулся с ним. Но это только потому, что Spock я использую исключительно для тестирования Java-приложений, т.е. я не любитель Groovy. Что мне нравится в этом подходе, так это то, что он работает без изучения исходного кода eachDir.
package de.scrum_master.stackoverflow

import groovy.mock.interceptor.MockFor
import spock.lang.Specification

class MockDirTest extends Specification {

  def "Mock eachDir indirectly via method stubbing"() {
    setup:
    File subDir = Mock() {
      // Stub all methods (in-)directly used by 'eachDir'
      getName() >>> ['subDir1', 'subDir2']
      lastModified() >> 2000
      exists() >> true
      isDirectory() >> true
    }
    File parentDir = Mock() {
      // Stub all methods (in-)directly used by 'eachDir'
      getName() >> 'parentDir'
      listFiles() >> [subDir, subDir]
      exists() >> true
      isDirectory() >> true
    }
    def helper = new DirectoryNameHelper()

    when:
    def result = helper.getUpperCaseDirectoryNames(parentDir)

    then:
    result == ['SUBDIR1', 'SUBDIR2']
  }

  def "Mock eachDir directly via MockFor.demand"() {
    setup:
    File subDir = Mock() {
      getName() >>> ['subDir1', 'subDir2' ]
    }
    def parentDir = new MockFor(File)
    parentDir.demand.eachDir { Closure closure ->
      closure(subDir)
      closure(subDir)
    }
    def helper = new DirectoryNameHelper()

    when:
    def result
    parentDir.use {
      result = helper.getUpperCaseDirectoryNames(new File('parentDir'))
    }

    then:
    result == ['SUBDIR1', 'SUBDIR2']
  }

  static class DirectoryNameHelper {
    List<String> getUpperCaseDirectoryNames(File dir) {
      List<String> names = []
      dir.eachDir { File f ->
        names << f.name.toUpperCase()
      }
      names
    }
  }

}
person kriegaex    schedule 19.04.2018
comment
Большое спасибо за это ... но должно быть что-то, чего я не понимаю: издевательство над getName(), exists(), isDirectory() и listFiles() в моем каталоге (dir в вашем классе DirectoryNameHelper) не повторяется через какие-либо (фиктивные) файлы ... Насколько я понял, Syzmon говорил, что мне нужно избавиться от eachDir и вместо этого использовать listFiles в коде моего приложения. Я также получаю InvalidSpecException: тип фиктивного объекта не может быть автоматически выведен из ваших строк Mock(). Я должен поставить Mock( File ) в обоих случаях. Возможно, мы используем разные версии Groovy? Я использую 2.6. - person mike rodent; 19.04.2018
comment
Однако мне нравится ваша идея изучить исходный код Groovy. Смотрите дополнение к моему вопросу... - person mike rodent; 19.04.2018
comment
@mikerodent Вы неправильно поняли мое предложение. Я не предлагал избавиться от eachDir из кода вашего приложения. Я предлагал не имитировать eachDir в вашем тестовом коде и имитировать метод listFiles(), который является поставщиком файлов внутри каталога для eachDir. И вы должны знать, что если вы решите издеваться над файлом, вам придется имитировать все его общедоступные методы для имитации определенных сценариев, таких как: файл — это каталог, файл — это текстовый файл, на который у меня нет разрешений и т. д. - person Szymon Stepniak; 19.04.2018
comment
Я использую Groovy 2.4.7. Глядя на артефакт Maven groovy-all, вы видите, что есть релиз-кандидат 2.5, 2.6 и 3.0 все еще находятся в альфа-состоянии. С ними мои тесты даже не компилируются, когда я только что их быстро протестировал. Для spock-core существует последняя версия 1.1-groovy-2.4. выпускать. Поэтому я рекомендую пока придерживаться Groovy 2.4. - person kriegaex; 20.04.2018
comment
Я забыл упомянуть: не только Maven Central говорит, что Groovy 2.6 находится в альфа-версии, но и groovy-lang. org/download.html. Так почему бы вам не сделать себе одолжение и не использовать стабильную версию вместо того, чтобы говорить трем парням, которые хотят вам помочь, что их код не работает? - person kriegaex; 20.04.2018
comment
Спасибо за подсказку про 2.4. Я не сказал трем парням, что их код не работает, я просто вежливо указал вам, что моя попытка запустить ваш код не сработала, и поинтересовался, не используем ли мы разные версии. - person mike rodent; 20.04.2018
comment
Я не говорил, что вы были невежливы. Но вы пытаетесь выполнить расширенное тестирование, и вам даже в голову не пришло упомянуть, что вы играете с передовой альфа-версией Groovy, пока у вас не возникли проблемы с запуском моего кода. Слишком много движущихся целей, чувак. Интересно, зачем любому опытному разработчику делать это, создавая для себя среду, которая усложняет отладку, а не облегчает при попытке попробовать что-то новое. Когда возникает проблема, никогда не знаешь, виновата она в тебе или в окружающей среде. - person kriegaex; 20.04.2018

Вы не можете имитировать eachDir таким образом, потому что этот метод не принадлежит классу File — он добавляется динамически через ResourceGroovyMethods. Вместо этого вам придется издеваться над методами listFiles(), exists() и isDirectory(), например:

    File mockParentDir = Mock(File) {
        getName() >> 'parent_dir'
        listFiles() >> mockSubdirs
        exists() >> true
        isDirectory() >> true
    }

Имитация методов exists() и isDirectory() является обязательной, потому что mock возвращает значения по умолчанию, если вы их не укажете, а для логического значения по умолчанию используется значение false — в этом случае вы получите FileNotFoundException. Вам придется сделать то же самое для mockSubdirs, если вы ожидаете, что он будет содержать каталоги.

Вот примерный тест, который показывает правильное издевательство:

import spock.lang.Specification

class MockDirSpec extends Specification {

    def "test mocked directories"() {
        setup:
        def mockSubdirs = []
        mockSubdirs << Mock( File ){
            getName() >> 'some subdir'
            lastModified() >> 2000
            exists() >> true
            isDirectory() >> true
        }

        File mockParentDir = Mock(File) {
            getName() >> 'parent_dir'
            listFiles() >> mockSubdirs
            exists() >> true
            isDirectory() >> true

        }

        def cut = new ClassUnderTest()
        cut.myDirectory = mockParentDir

        when:
        def names = cut.names()

        then:
        names == ['some subdir']
    }

    static class ClassUnderTest {
        File myDirectory

        List<String> names() {
            def dirNames = []
            myDirectory.eachDir {
                dirNames << it.name
            }
            return dirNames
        }
    }
}

Насмешка eachDir - недостатки

Когда дело доходит до имитации функции eachDir, есть один существенный недостаток. Это довольно специфическая функция по определению - она ​​перебирает только подкаталоги. Это означает, что эта часть кода приложения из вашего примера:

def dirNames = []
myDirectory.eachDir{ 
    dirNames << it.name
}

дает разные результаты в зависимости от того, на что ссылается переменная myDirectory. Например:

  • если myDirectory указывает на пустой каталог, dirNames оказывается пустым
  • если myDirectory указывает на каталог, содержащий несколько текстовых файлов, dirNames оказывается пустым
  • если myDirectory указывает на каталог, содержащий 2 подкаталога и 10 текстовых файлов, dirNames в конечном итоге содержит 2 элемента, имена этих подкаталогов

Если мы имитируем eachDir, чтобы он всегда принимал одни и те же фиксированные входные файлы, не имеет значения, вызываем ли мы его для переменной, представляющей пустой каталог, или для каталога, содержащего 2 подкаталога и несколько текстовых файлов - результат в обоих случаях всегда один и тот же.

В этом случае для меня имеет больше смысла издеваться над вводом — каталогом, представленным как File. Благодаря этому вы можете моделировать без создания реального файла:

  • пустой каталог
  • каталог с одним текстовым файлом
  • каталог с одним подкаталогом
  • каталог с кучей подкаталогов и несколькими текстовыми файлами
  • и т.п.

И вам не нужно имитировать поведение метода eachDir, что является огромным преимуществом.

Еще одним преимуществом является то, что вам не нужно менять код приложения — вы по-прежнему можете использовать функцию eachDir внутри. Когда вы имитируете входные файлы вместо имитирования метода eachDir, вы просто предоставляете тестовые данные, которые хранятся в памяти, а не в файловой системе. Представьте себе создание желаемой файловой структуры и изучение того, чем эти экземпляры File представлены во время выполнения с помощью отладчика — вы можете воспроизвести то, что возвращают все общедоступные методы из класса File, используя значения, взятые из реальной файловой системы. Это может дать вам хорошую симуляцию «в памяти» того, как выглядит конкретный каталог при сохранении в файловой системе. И вы используете его в качестве входных данных в своем тесте для имитации того, что происходит во время выполнения. Вот почему я считаю насмешку над eachDir вредной — она создает сценарий, который не отображается в среде выполнения.

Также есть хороший пост в блоге от дяди Боба о насмешках, который можно резюмировать следующим выводом:

"Вкратце, однако, я рекомендую вам экономно использовать имитацию. Найдите способ тестирования — спроектируйте способ тестирования — вашего кода, чтобы он не требовал имитации. Зарезервируйте имитацию для архитектурно значимых границ; и тогда будьте безжалостны. Это важные границы вашей системы, и ими нужно управлять не только для тестов, но и для всего."

Источник: https://8thlight.com/blog/uncle-bob/2014/05/10/WhenToMock.html

person Szymon Stepniak    schedule 18.04.2018
comment
Вы не можете издеваться над каждым Dir - я не думаю, что это правда. - person Jeff Scott Brown; 18.04.2018
comment
Существует несколько способов имитировать метод eachDir. MockFor один. - person Jeff Scott Brown; 18.04.2018
comment
@JeffScottBrown Я имел в виду то, как OP представлен в вопросе. Определение eachDir() или eachDir(_) внутри закрытия Mock(File) не имеет никакого эффекта, я проверил это на примере, показанном в ответе. С другой стороны, издевательство над этим методом не имело бы никакого смысла. Он потребляет замыкание и ничего не возвращает. - person Szymon Stepniak; 18.04.2018
comment
Вы можете имитировать метод и заставить фиктивную реализацию вызывать указанное закрытие с тем, что тест хочет предоставить закрытию. Я думаю, что тест будет проверять побочные эффекты закрытия, вызываемого с определенными данными, а не тем, что возвращает eachDir. - person Jeff Scott Brown; 18.04.2018
comment
Я не думаю, что это хорошая идея. В этом примере я вижу насмешку как способ предоставить альтернативу файловой системе и полагаться на реальную реализацию метода eachDir, которая довольно специфична (перебирает подкаталоги заданного каталога). Я ожидаю, что для каталога, содержащего 2 подкаталога и 5 текстовых файлов, этот метод выполняет данное закрытие дважды, только для подкаталогов. Если я издеваюсь над eachDir, я могу изменить поведение этого метода, как показано ниже, когда издевательство над eachDir принимает файлы, которые не являются каталогами. Я бы не назвал это хорошим примером модульного теста. - person Szymon Stepniak; 18.04.2018
comment
@SzymonStepniak Спасибо за ваше объяснение и предложение, как на это смотреть ... но я не могу согласиться с вашим утверждением, что это не имеет смысла, потому что оно потребляет замыкание и ничего не возвращает. Это слишком узкое требование для квалификации макета как действительного, как предполагает Джефф, и мой пример действительно иллюстрирует почему. Ваше возражение о том, что вы можете изменить поведение, также не имеет смысла: все насмешки надуманы по своей природе и тесно зависят от кода приложения. Наконец, неправильно предполагать, что это юнит-тест: не менее важны функциональные тесты. - person mike rodent; 19.04.2018
comment
Майк, при всем уважении: Ваш последний комментарий не имеет для меня особого смысла. Не все насмешки придуманы природой. Тестирование в целом становится надуманным, только если надуманным является тестируемый код. Необходимость вообще смотреть исходный код приложения для тестирования или насмешки — это запах и причина для рефакторинга приложения для лучшей тестируемости, например. через внедрение зависимостей. Кроме того, тот факт, что ваш тест является функциональным тестом, а не модульным тестом, ни в малейшей степени не делает комментарий Шимона недействительным. - person kriegaex; 19.04.2018
comment
О, и, кстати, для функционального теста никто не мешает вам создать структуру каталогов, которую вы хотите протестировать, в качестве тестовых ресурсов с управлением версиями. Вы даже можете манипулировать датами файлов по своему вкусу при настройке тестового прибора. Или просто зафиксируйте файлы в том порядке, в котором они вам нужны. Если ничего не помогает, вы все равно можете просто смоделировать/заглушить метод, возвращающий дату файла. Как бы то ни было, есть много способов добиться того, чего вы хотите, даже не издеваясь над всей файловой системой. Тогда у вас есть настоящий интеграционный тест. - person kriegaex; 19.04.2018
comment
@mikerodent Мое беспокойство по поводу насмешек над eachDir имеет другой источник. Использование макета файла в тесте полезно во многих случаях — вы можете имитировать размер файла, вы можете имитировать его жизненный цикл (создание, обновление, удаление), не заботясь о том, изменяется ли реальный файл другим процессом и т. д. Однако имитация eachDir имеет один существенный недостаток - если вы издеваетесь над ним, не имеет значения, запускаете ли вы его в пустом каталоге, в каталоге, содержащем только текстовые файлы, или в каталоге, содержащем 2 подкаталога и несколько текстовых файлов. В этом случае вы теряете возможность тестировать эти угловые случаи. Я согласен, что у вас другое мнение. - person Szymon Stepniak; 19.04.2018
comment
@kriegaex и Шимон Степняк - спасибо за ваши экспертные комментарии. Я прочитал их обоих очень внимательно. Вещи, которых я до сих пор не понимаю: отказаться от использования функции Groovy (и вместо этого использовать listFiles в коде приложения) кажется немного пораженческим... и я не вижу, как я могу имитировать/заглушить метод, возвращающий дата файла, если (например) я не знаю, какие основные методы Java (если есть!) используются eachDir ... (each OTOH потенциально может быть легче замаскировать в этом смысле). Конечно, ответ на возражение Шимона состоит в том, чтобы отфильтровать не-каталоги в вашем фиктивном методе замены...? - person mike rodent; 19.04.2018
comment
Подумайте об этом таким образом - создайте каталог с подкаталогами и файлами в нем, временно используйте его в тесте вместо моков, исследуйте с помощью отладчика, какие реальные файлы возвращают для каждого File общедоступного метода, и воспроизведите те же значения с помощью моков. Это позволит вам полностью имитировать представление каталога файловой системы, не касаясь файловой системы. Вам не нужно отказываться от каких-либо функций Groovy. Даже не важно, что eachDir делает под капотом. Если вы не хотите использовать настоящую файловую систему, вам нужно как можно точнее имитировать ее с помощью фиктивных файлов, вот и все. - person Szymon Stepniak; 19.04.2018
comment
И вам не нужно ничего менять в коде вашего приложения. Цель состоит в том, чтобы предоставить тестовые данные, которые имитируют то, что происходит или может произойти, когда ваше приложение работает. eachDir за этой функцией стоит некоторая бизнес-логика. Ваше приложение не имитирует его во время выполнения, оно полагается на его реализацию, поэтому я предлагаю не имитировать его в тесте. В противном случае ваш тест больше не представляет вариант использования во время выполнения, и вы на самом деле не знаете, что может произойти, когда приложение работает. Имитация входных данных позволяет вам моделировать крайние случаи, с которыми вы хотите протестировать метод eachDir. - person Szymon Stepniak; 19.04.2018
comment
Извините, но я все еще не понимаю! Исходный код Groovy, как видно из моего дополнения, использует Files.newDirectoryStream для получения своего содержимого (файлов и каталогов). Поэтому недостаточно охватить все основы File, то есть общедоступные методы. - person mike rodent; 19.04.2018