Spock提供了3个功能强大(但本质上有所不同)的工具,可简化编写测试的过程:Mock,Stub和Spy。
通常,需要测试的代码需要与称为依赖项的外部模块进行交互(原始文章使用协作者一词,在俄语环境中并不常见)。
单元测试通常设计为使用mok的各种变体(模拟,存根和间谍)测试单个隔离的类。 因此,随着依赖代码的发展,测试将更加可靠,并且不会破裂。
当更改依赖关系实现的内部细节时,此类隔离的测试不太容易出现问题。
译者的话:每次我使用Spock框架编写测试时,我都觉得选择替代依赖的方式时会犯错。 本文提供了最短的备忘单,用于选择创建Mokas的机制。
TL; DR
嘲弄
使用模拟来:
- 测试代码和依赖项之间的合同检查
- 检查依赖方法的正确次数
- 验证依赖代码被调用的参数
存根
使用存根可以:
- 提供预定义的通话结果
- 执行依赖项期望的预定义操作,例如引发异常
间谍
恐惧间谍。 正如Spock文档所述:
使用此机制前请三思。 也许您应该重新设计解决方案并重新组织代码。
但是碰巧的是,在某些情况下,我们不得不使用遗留代码。 旧版代码可能很难甚至不可能用暴民和存根进行测试。 在这种情况下,只有一种解决方案:使用Spy。
最好让旧代码包含在使用Spy的测试中,而不要完全没有旧测试。
使用间谍可以:
- 测试无法通过其他方法测试的旧版代码
- 检查依赖方法的正确次数
- 验证传递的参数
- 提供预定义的依赖项响应
- 执行预定义的操作以响应对依赖方法的调用
嘲弄
当单元测试的任务是验证被测代码与依赖项之间的约定时,moxas的所有功能都得到体现。 让我们看下面的示例,其中有一个使用FooService
作为依赖项的FooController
控制器,并通过FooService
测试了此功能。
FooController.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } }
FooService.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } }
在这种情况下,我们要编写一个测试来测试:
FooController
和FooService
之间的合同FooService.doSomething(name)
被调用的正确次数- 使用正确的参数调用
FooService.doSomething(name)
看一下测试:
MockSpec.groovy
package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class MockSpec extends Specification implements ControllerUnitTest<FooController> { void "Mock FooService"() { given: " " def fooService = Mock(FooService) and: " " controller.fooService = fooService when: " " controller.doSomething() then: " " 1 * fooService.doSomething("Sally") and: " '' - 'null'" response.text == null.toString() } }
上面的测试创建了一个服务模拟:
def fooService = Mock(FooService)
该测试还检查FooService.doSomething(name)
是否被调用一次,并且传递给它的参数与字符串"Sally"
匹配。
1 * fooService.doSomething("Sally")
上面的代码解决了4个重要问题:
- 为
FooService
创建模拟 - 验证是否使用
String
参数和值"Sally"
恰好一次调用了FooService.doSomething(String name)
- 隔离测试代码,替换依赖关系实现
存根
被测试的代码是否使用依赖关系? 测试的目的是为了确保与依赖项进行交互时,被测代码能够正常工作? 依赖方法调用的结果是否输入了被测代码的值?
如果测试代码的行为根据依赖项的行为而变化,则需要使用存根(Stub)。
让我们看下面的FooController
和FooService
示例,并使用存根测试控制器功能。
FooController.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } }
FooService.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } }
测试代码:
StubSpec.groovy
package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class StubSpec extends Specification implements ControllerUnitTest<FooController> { void "Stub FooService"() { given: " " def fooService = Stub(FooService) { doSomething(_) >> "Stub did something" } and: " " controller.fooService = fooService when: " " controller.doSomething() then: " "
您可以这样创建一个存根:
def fooService = Stub(FooService) { doSomething(_) >> "Stub did something" }
上面的代码解决了4个重要问题:
- 创建一个存根
FooService
- 确保无论传递的参数如何,
FooService.doSomething(String name)
都将返回字符串"Stub did something"
(因此我们使用了_
字符) - 隔离测试的代码,用存根替换依赖实现
间谍
请不要阅读本节。
不要看
跳到下一个。
还在读书吗? 好吧,好的,让我们处理Spy。
不要使用间谍。 正如Spock文档所述:
使用此机制前请三思。 也许您应该重新设计解决方案并重新组织代码。
在这种情况下,有些情况下我们不得不使用遗留代码。 旧版代码无法使用小怪或存根进行测试。 在这种情况下,间谍仍然是唯一可行的选择。
间谍与mokas或stub有所不同,因为它们不像stub那样工作。
用mok或stub替换依赖项时,将创建测试对象,并且不执行依赖项的实际源代码。
另一方面,间谍将执行为其创建间谍的依赖项的主要源代码,但间谍将允许您更改间谍返回的内容并检查方法调用,就像mokas和stub一样。 (因此命名为Spy)。
让我们看下面的FooController
示例,该示例使用FooService
,然后使用间谍测试功能。
FooController.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooController { FooService fooService def doSomething() { render fooService.doSomething("Sally") } }
FooService.groovy
package com.mycompany.myapp import groovy.transform.CompileStatic @CompileStatic class FooService { String doSomething(String name) { "Hi ${name}, FooService did something" } }
测试代码:
SpySpec.groovy
package com.mycompany.myapp import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class SpySpec extends Specification implements ControllerUnitTest<FooController> { void "Spy FooService"() { given: " -" def fooService = Spy(FooService) and: " " controller.fooService = fooService when: " " controller.doSomething() then: " " 1 * fooService.doSomething("Sally") >> "A Spy can modify implementation" and: ' ' response.text == "A Spy can modify implementation" } }
创建一个间谍实例非常简单:
def fooService = Spy(FooService)
在上面的代码中,间谍程序使我们可以检查对FooService.doSomething(name)
的调用,调用数量和参数值。 而且,间谍更改了方法的实现以返回不同的值。
1 * fooService.doSomething("Sally") >> "A Spy can modify implementation"
上面的代码解决了4个重要问题:
- 为
FooService
创建一个间谍实例 - 检查依赖项交互
- 根据依赖方法调用的某些结果检查应用程序的工作方式
- 隔离测试的代码,用存根替换依赖实现
常见问题
使用哪个选项:模拟,存根或间谍?
这是许多开发人员面临的问题。 如果不确定使用哪种方法,此常见问题解答可以提供帮助。
问:测试的目的是为了验证被测代码与依赖项之间的契约?
答:如果回答是,请使用模拟
问:测试的目的是确保与依赖项进行交互时被测代码能够正常工作吗?
答:如果回答是,请使用存根
问:调用依赖方法的结果是否输入了被测代码的值?
答:如果回答是,请使用存根
问:您是否使用旧代码,这很难测试,并且没有其他选择?
答:尝试使用间谍
代码示例
您可以在以下位置找到本文所有示例的代码:
https://github.com/ddelponte/mock-stub-spy
有用的链接