Spock - 模拟 Groovy 闭包方法?

Spock - mock a Groovy closure method?

这就是我想要做的:

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

app代码是这样的:

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

以上所有在 myDirectory.eachDir{ 行给出 FileNotFoundException...

以后

感谢所有 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()
}

...沿着这些方向的尝试似乎不起作用...听到有人说 [=58] 中的 Files class 我一点也不会感到惊讶=] 语言机制不会受到这种模拟尝试的影响...

然后我想大概 toPath 必须在有问题的 File 上调用,所以尝试了这个:

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

...此行未打印。好吧,我有点难过:要从 File 得到一个 Path 我给它 Groovy 机制必须使用一些偷偷摸摸的东西:也许像 getAbsolutePath()...然后从结果 String 创建一个 Path?这将需要对源代码进行更多检查……但如果是这种情况,您将无法强制 Groovy 使用模拟 Path! 或者...也许其他神秘的 Groovy 事情在这里发挥作用:metaclass,等等?

你不能那样模拟 eachDir,因为这个方法不属于 File class - 它是通过 ResourceGroovyMethods [=86= 动态添加的].您将不得不模拟 listFiles()exists()isDirectory() 方法,例如:

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

模拟 exists()isDirectory() 方法是强制性的,因为模拟 return 的默认值,如果你不指定一个和布尔值的默认值是 false - 在这种情况下,您将得到 FileNotFoundException。如果您希望 mockSubdirs 包含目录,则必须对 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 class [=] 中的所有 public 方法91=] 使用取自真实文件系统的值。这可以很好地 "in-memory" 模拟特定目录存储在文件系统中时的样子。您将其用作测试中的输入数据,以模拟运行时发生的情况。这就是为什么我认为模拟 eachDir 有害 - 它创建了一个不会出现在运行时的场景。

Bob 叔叔还有一篇关于模拟的好博客 post,可以总结为以下结论:

"In short, however, I recommend that you mock sparingly. Find a way to test -- design a way to test -- your code so that it doesn't require a mock. Reserve mocking for architecturally significant boundaries; and then be ruthless about it. Those are the important boundaries of your system and they need to be managed, not just for tests, but for everything."

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

这取决于您真正想要测试的是什么。这是一个可能有用的示例:

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 并传递一个闭包,其中 returns 每个目录名称的大写版本。

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

首先,我想对 Szymon Stepniak and Jeff Scott Brown 两位的回答表示感谢,他们的回答都非常有见地,因此我都投了赞成票。我建议 OP 接受他最喜欢的一种, 而不是 这个,因为在这里我只是将两种方法统一到一个规范中,使用相同的 class 进行测试以及特征方法中的可比较变量命名。我还简化了子目录的模拟使用,只使用一个模拟对象,该对象在通过 getName() >>> ['subDir1', 'subDir2'].

的后续调用中 returns 两个不同的文件名

所以现在我们可以更轻松地比较两种基本上这样做的方法:

  • Szymon 的方法是依赖板载 Spock 方法,并且是测试 Java classes 时应该使用的方法。 OTOH,我们在这里处理 eachDir,一个 Groovy 特定的东西。这里的缺点是,为了实现这种模拟,我们确实需要查看 eachDir 的源代码及其辅助方法之一,以便找出究竟需要存根的内容,以便一切正常。尽管如此,它仍然是简单明了且有效的解决方案 IMO。
  • Jeff 的方法将 Spock 模拟与 Groovy 自己的 MockFor 混合在一起,这让我在第一次遇到它时有点难以阅读。但这只是因为我专门使用 Spock 来测试 Java 应用程序,即我不是 Groovy 爱好者。我喜欢这种方法的原因是它无需查看 eachDir 的源代码即可工作。
package de.scrum_master.Whosebug

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

}