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.newDirectoryStream
。 Files
是 final
,因此您必须使用 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
}
}
}
这就是我想要做的:
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.newDirectoryStream
。 Files
是 final
,因此您必须使用 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']
.
所以现在我们可以更轻松地比较两种基本上这样做的方法:
- 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
}
}
}