定义 Spock 模拟行为
Defining Spock mock behaviors
我正在编写我的第一个 Spock 测试并阅读 mocking interactions 上的文档,但我仍然没有在一些项目上看到 "forest through the trees"。
我有一个 class、MyRealm
,可以为我的应用程序执行身份验证。它有两个依赖项,AuthService
和 ShiroAdapter
。前者我想嘲笑,后者我想保持原样(如果可能的话)。这是因为 AuthService
实际上与 LDAP 建立了后端连接,所以我想模拟它。但是 ShiroAdapter
只是定义了几个将我的对象转换为 Apache Shiro 安全对象(主体、权限等)的实用方法。所以它可以不被嘲笑(methinks)。
class MyRealmSpec extends Specification {
MyRealm realm
def setup() {
AuthService authService = Mock(AuthService)
// configure 'authService' mock <-- ?????
ShiroAdapter shiroAdapter = new ShiroAdapter()
realm = new MyRealm(authService: authService,
shiroAdapter: shiroAdapter)
}
def "authenticate throws ShiroException whenever auth fails"() {
when:
realm.authenticate('invalid_username', 'invalid_password')
then:
Throwable throwable = thrown()
ShiroException.isAssignableFrom(throwable)
}
}
我相信我非常接近,但是我正在努力配置模拟以按照我希望的方式运行以进行测试。 Spock 文档(上面链接)似乎只记录了如何验证调用模拟方法的次数。我对这里不感兴趣。
在这里,MyRealm#authenticate(String,String)
在后台调用 AuthService#doAuth(String,String)
。所以我需要我的模拟 AuthService
实例来简单地 return false
(表示失败的身份验证)或者如果发生意外情况则抛出 ServiceFaulException
。
有什么办法可以做到这一点吗?
你非常接近,检查抛出的异常类型的一种简单、快捷的方法是将 Exception
class 放在括号中。例如:
def "authenticate throws ShiroException whenever auth fails"() {
when:
realm.authenticate('invalid_username', 'invalid_password')
then:
thrown(ShiroException)
}
您还需要模拟 LDAP 服务调用本身并模拟异常或登录失败。模拟操作进入测试的 then 子句。
def "authenticate throws ShiroException whenever auth fails"() {
setup:
String invalidUserName = 'invalid_username'
String invalidPassword = 'invalid_password'
when:
realm.authenticate(invalidUserName, invalidPassword)
then:
1 * authService.doAuth(invalidUserName, invalidPassword) >> returnClosure
thrown(ShiroException)
where:
returnClosure << [{throw new ShiroException()}, { false }]
}
请注意,您需要使模拟语句中的参数匹配或使用通配符匹配。
要匹配任何字符串,您可以使用下划线语法:
1 * authService.doAuth(_, _) >> false
您可能会对一些不同的行为对象感兴趣。
存根 - 您只需定义 returned
的内容
MyObject obj = Stub{method >> null}
Mocks - 您可以定义 returned and/or 方法被调用的次数
MyObject obj = Mock {1..3 methodCall >> false}
间谍 - 它会创建您的对象,但您可以将特定方法作为模拟覆盖(并且您的覆盖仍然可以调用原始代码)
MyObject obj = Spy {methodCall >> false}
obj.otherMethodCall() // Calls code like normal
obj.methodCall() // Returns false like we told it to
听起来您需要一个存根,但您可以毫无问题地使用模拟。我提到间谍,因为如果你的对象是独立的(在未来),它是一个救命稻草。
def "authenticate throws ShiroException whenever auth fails"() {
given:
AuthService authService = Stub(AuthService)
authService.doAuth(_,_) >> expectedError
MyRealm realm = new MyRealm(
authService: authService,
shiroAdapter: new ShiroAdapter())
when:
realm.authenticate("just enough to get", "to the doAuth method")
then:
thrown(ShiroException)
where:
expectedError << [ShiroException, /*other exceptions this method has to test*/]
}
不需要 data/logic 分隔,但这是使测试更加灵活和可维护的好方法。尽管在这种情况下它并不是真正需要的,因为您只有一个异常可以抛出。
我实际上会将失败的身份验证测试和例外的身份验证测试分开。他们正在研究根本不同的行为,并且测试这两种情况的测试逻辑有些不同。为了 maintainability/flexibility 的利益,避免每次测试过多(或过少)的测试符合您的利益。
def "authenticate throws ShiroException whenever auth fails"() {
given:
AuthService authService = Stub(AuthService)
authService.doAuth(_,_) >> { args ->
return args[0] == good && args[1] == good
}
MyRealm realm = new MyRealm(
authService: authService,
shiroAdapter: new ShiroAdapter())
expect:
realm.authenticate(username, password) == expectedAuthentication
where:
userName | password | expectedAuthentication
bad | good | false
bad | bad | false
good | good | true
}
注意上面的测试,这个测试...
- return 值的模拟计算(测试测试)
- 调用 authenticate 和 doAuth() 之间发生的任何代码
希望这就是您的意图。如果 .authenticate() 的逻辑中没有任何可以破坏的内容(它的复杂性与 getter 或 setter 方法相当),则此测试主要是浪费时间。逻辑可能中断的唯一方法是 JVM 出现问题(这完全不在本测试的责任范围内),或者有人在将来某个时候进行了更改(好吧,即使假设 .authenticate() 包含牢不可破的基本逻辑测试具有一定的价值)。我漫无边际的题外话(非常抱歉);确保牢记测试的内容和原因。它将帮助您确定测试用例的优先级,同时找出 organize/separate 测试逻辑的最佳方法。
我正在编写我的第一个 Spock 测试并阅读 mocking interactions 上的文档,但我仍然没有在一些项目上看到 "forest through the trees"。
我有一个 class、MyRealm
,可以为我的应用程序执行身份验证。它有两个依赖项,AuthService
和 ShiroAdapter
。前者我想嘲笑,后者我想保持原样(如果可能的话)。这是因为 AuthService
实际上与 LDAP 建立了后端连接,所以我想模拟它。但是 ShiroAdapter
只是定义了几个将我的对象转换为 Apache Shiro 安全对象(主体、权限等)的实用方法。所以它可以不被嘲笑(methinks)。
class MyRealmSpec extends Specification {
MyRealm realm
def setup() {
AuthService authService = Mock(AuthService)
// configure 'authService' mock <-- ?????
ShiroAdapter shiroAdapter = new ShiroAdapter()
realm = new MyRealm(authService: authService,
shiroAdapter: shiroAdapter)
}
def "authenticate throws ShiroException whenever auth fails"() {
when:
realm.authenticate('invalid_username', 'invalid_password')
then:
Throwable throwable = thrown()
ShiroException.isAssignableFrom(throwable)
}
}
我相信我非常接近,但是我正在努力配置模拟以按照我希望的方式运行以进行测试。 Spock 文档(上面链接)似乎只记录了如何验证调用模拟方法的次数。我对这里不感兴趣。
在这里,MyRealm#authenticate(String,String)
在后台调用 AuthService#doAuth(String,String)
。所以我需要我的模拟 AuthService
实例来简单地 return false
(表示失败的身份验证)或者如果发生意外情况则抛出 ServiceFaulException
。
有什么办法可以做到这一点吗?
你非常接近,检查抛出的异常类型的一种简单、快捷的方法是将 Exception
class 放在括号中。例如:
def "authenticate throws ShiroException whenever auth fails"() {
when:
realm.authenticate('invalid_username', 'invalid_password')
then:
thrown(ShiroException)
}
您还需要模拟 LDAP 服务调用本身并模拟异常或登录失败。模拟操作进入测试的 then 子句。
def "authenticate throws ShiroException whenever auth fails"() {
setup:
String invalidUserName = 'invalid_username'
String invalidPassword = 'invalid_password'
when:
realm.authenticate(invalidUserName, invalidPassword)
then:
1 * authService.doAuth(invalidUserName, invalidPassword) >> returnClosure
thrown(ShiroException)
where:
returnClosure << [{throw new ShiroException()}, { false }]
}
请注意,您需要使模拟语句中的参数匹配或使用通配符匹配。
要匹配任何字符串,您可以使用下划线语法:
1 * authService.doAuth(_, _) >> false
您可能会对一些不同的行为对象感兴趣。
存根 - 您只需定义 returned
的内容MyObject obj = Stub{method >> null}
Mocks - 您可以定义 returned and/or 方法被调用的次数
MyObject obj = Mock {1..3 methodCall >> false}
间谍 - 它会创建您的对象,但您可以将特定方法作为模拟覆盖(并且您的覆盖仍然可以调用原始代码)
MyObject obj = Spy {methodCall >> false} obj.otherMethodCall() // Calls code like normal obj.methodCall() // Returns false like we told it to
听起来您需要一个存根,但您可以毫无问题地使用模拟。我提到间谍,因为如果你的对象是独立的(在未来),它是一个救命稻草。
def "authenticate throws ShiroException whenever auth fails"() {
given:
AuthService authService = Stub(AuthService)
authService.doAuth(_,_) >> expectedError
MyRealm realm = new MyRealm(
authService: authService,
shiroAdapter: new ShiroAdapter())
when:
realm.authenticate("just enough to get", "to the doAuth method")
then:
thrown(ShiroException)
where:
expectedError << [ShiroException, /*other exceptions this method has to test*/]
}
不需要 data/logic 分隔,但这是使测试更加灵活和可维护的好方法。尽管在这种情况下它并不是真正需要的,因为您只有一个异常可以抛出。
我实际上会将失败的身份验证测试和例外的身份验证测试分开。他们正在研究根本不同的行为,并且测试这两种情况的测试逻辑有些不同。为了 maintainability/flexibility 的利益,避免每次测试过多(或过少)的测试符合您的利益。
def "authenticate throws ShiroException whenever auth fails"() {
given:
AuthService authService = Stub(AuthService)
authService.doAuth(_,_) >> { args ->
return args[0] == good && args[1] == good
}
MyRealm realm = new MyRealm(
authService: authService,
shiroAdapter: new ShiroAdapter())
expect:
realm.authenticate(username, password) == expectedAuthentication
where:
userName | password | expectedAuthentication
bad | good | false
bad | bad | false
good | good | true
}
注意上面的测试,这个测试...
- return 值的模拟计算(测试测试)
- 调用 authenticate 和 doAuth() 之间发生的任何代码
希望这就是您的意图。如果 .authenticate() 的逻辑中没有任何可以破坏的内容(它的复杂性与 getter 或 setter 方法相当),则此测试主要是浪费时间。逻辑可能中断的唯一方法是 JVM 出现问题(这完全不在本测试的责任范围内),或者有人在将来某个时候进行了更改(好吧,即使假设 .authenticate() 包含牢不可破的基本逻辑测试具有一定的价值)。我漫无边际的题外话(非常抱歉);确保牢记测试的内容和原因。它将帮助您确定测试用例的优先级,同时找出 organize/separate 测试逻辑的最佳方法。