定义 Spock 模拟行为

Defining Spock mock behaviors

我正在编写我的第一个 Spock 测试并阅读 mocking interactions 上的文档,但我仍然没有在一些项目上看到 "forest through the trees"。

我有一个 class、MyRealm,可以为我的应用程序执行身份验证。它有两个依赖项,AuthServiceShiroAdapter。前者我想嘲笑,后者我想保持原样(如果可能的话)。这是因为 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
}

注意上面的测试,这个测试...

  1. return 值的模拟计算(测试测试)
  2. 调用 authenticate 和 doAuth() 之间发生的任何代码

希望这就是您的意图。如果 .authenticate() 的逻辑中没有任何可以破坏的内容(它的复杂性与 getter 或 setter 方法相当),则此测试主要是浪费时间。逻辑可能中断的唯一方法是 JVM 出现问题(这完全不在本测试的责任范围内),或者有人在将来某个时候进行了更改(好吧,即使假设 .authenticate() 包含牢不可破的基本逻辑测试具有一定的价值)。我漫无边际的题外话(非常抱歉);确保牢记测试的内容和原因。它将帮助您确定测试用例的优先级,同时找出 organize/separate 测试逻辑的最佳方法。