Spock - 模拟一个Groovy闭包方法?

时间:2018-04-18 18:34:46

标签: groovy mocking closures spock gdk

这就是我想要做的事情:

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.newDirectoryStreamFilesfinal,因此您必须使用GroovyMock,因为该方法为static,您似乎必须使用以下内容:

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

...沿着这些方向尝试似乎不起作用......听到有人认为Groovy语言机制中的Files类不会受此影响,我不会感到惊讶试图模仿...

然后我认为必须在问题的toPath上调用File,所以尝试了这个:

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

...此行未打印。好吧,我有点难过:要从Path获得File我给它Groovy机制必须使用偷偷摸摸的东西:也许像getAbsolutePath() ...然后创建一个结果Path中的String?这需要更多的源代码检查......但如果是这种情况,那么你就不能强迫Groovy使用模拟Path! 或者......也许其他神秘的Groovy事情在这里发挥作用:元类等?

3 个答案:

答案 0 :(得分:2)

这取决于你真正想要测试的内容。以下是一个可能有用的示例:

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

答案 1 :(得分:1)

您不能以这种方式模仿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类的所有公共方法返回的内容。这可以给你一个很好的内存和#34;模拟存储在文件系统中时特定目录的外观。并且您将其用作测试中的输入数据,以模拟运行时中发生的情况。这就是我考虑模仿eachDir有害的原因 - 它会创建一个没有出现在运行时的场景。

鲍勃叔叔还有一篇关于嘲笑的好博文,可以通过以下结论进行总结:

  

&#34;但是,简而言之,我建议你谨慎地嘲笑。找到一种测试方法 - 设计一种测试方法 - 您的代码,以便它不需要模拟。保留嘲弄建筑上重要的界限;然后对此无情。这些是您系统的重要边界,需要对它们进行管理,不仅仅是为了测试,而是为了一切。&#34;

     

来源:https://8thlight.com/blog/uncle-bob/2014/05/10/WhenToMock.html

答案 2 :(得分:1)

首先,我要感谢Szymon StepniakJeff Scott Brown各自的答案,这些答案都非常有见地,而且由于这个原因我都投了赞成票。我建议OP接受他最喜欢的一个,这个,因为在这里我只是将两种方法统一到一个规范中,使用相同的测试类和功能中的可比较变量命名方法。我还简化了子目录的模拟使用,只使用一个模拟对象,该对象在后续调用时通过getName() >>> ['subDir1', 'subDir2']返回两个不同的文件名。

所以现在我们可以更容易地比较基本上这两种方法:

  • Szymon的方法是依靠板载Spock手段,是测试Java类时应该使用的方法。 OTOH,我们正在处理eachDir,这是一个特定于Groovy的东西。这里的缺点是,为了实现这种模拟,我们真的需要查看eachDir的源代码及其辅助方法之一,以便找出究竟需要存根的内容以便制作一切正常。尽管如此,它仍然是直截了当且有效的解决方案IMO。
  • Jeff的方法将Spock模拟与Groovy自己的MockFor混合在一起,让我在第一次遇到它时更难以阅读。但这只是因为我专门使用Spock来测试Java应用程序,即我不是Groovy buff。我对这种方法的喜欢之处在于它可以在不查看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
    }
  }

}