【问题标题】:Defining Spock mock behaviors定义 Spock 模拟行为
【发布时间】:2015-10-10 10:17:34
【问题描述】:

我正在编写我的第一个 Spock 测试并阅读 mocking interactions 上的文档,但我仍然没有在一些项目上看到“树林中的森林”。

我有一个类MyRealm,它为我的应用程序执行身份验证。它有两个依赖项,AuthServiceShiroAdapter。前者我想嘲笑,后者我想保持原样(如果可能的话)。这是因为 AuthService 实际上与 LDAP 建立了后端连接,所以我想模拟它。但是ShiroAdapter 只是定义了几个实用方法,将我的对象转换为 Apache Shiro 安全对象(主体、权限等)。所以它可以不被嘲笑(我认为)。

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 实例来简单地返回 false(表示身份验证失败)或在发生意外情况时抛出 ServiceFaulException

有什么想法可以做到这一点吗?

【问题讨论】:

    标签: groovy spock


    【解决方案1】:

    您非常接近,检查抛出的异常类型的一种简单、快捷的方法是将Exception 类放在括号中。例如:

    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
    

    【讨论】:

    • 感谢@Durandal (+1) - 如何在then 标签中实现“OR”逻辑?我将如何改变事情以实现“AND”逻辑呢?再次感谢!
    • AND 隐含在then 子句中,每个模拟调用或列出的条件都按提供的顺序检查;我只是将 OR 放在那里以显示返回异常与返回布尔值的示例。您可以添加一个where 子句在同一个测试中执行这两项操作。我会更新以添加该案例。
    【解决方案2】:

    您可能对一些不同的行为对象感兴趣。

    • 存根 - 您只定义返回的内容

      MyObject obj = Stub{method >> null}
      
    • Mocks - 您定义返回的内容和/或调用方法的次数

      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*/] 
    }
    

    不需要数据/逻辑分离,但它是使测试更加灵活和可维护的好方法。虽然在这种情况下它并不是真正需要的,因为你只有一个异常可以抛出。

    我实际上会将您的失败身份验证和异常身份验证测试分开。他们正在研究根本不同的行为,并且测试这两种情况的测试逻辑有些不同。为了可维护性/灵活性,避免每次测试过多(或过少)测试符合您的利益。

    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. mock 的返回值计算(测试测试)
    2. 在调用 authenticate 和 doAuth() 之间发生的任何代码

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

    【讨论】:

    • 你能发布参考吗?
    猜你喜欢
    • 2018-01-03
    • 2019-08-31
    • 1970-01-01
    • 2017-02-22
    • 1970-01-01
    • 2015-08-03
    • 1970-01-01
    • 2022-06-11
    相关资源
    最近更新 更多