【问题标题】:groovy spock testing closure with spygroovy spock 用 spy 测试闭包
【发布时间】:2020-02-03 17:16:52
【问题描述】:

我有调用管道步骤方法(withCredentials)的共享库。我正在尝试在调用 myMethodToTest 时使用 sh 脚本正确调用 withCredentials 方法,但在迭代 withCredentials 闭包时遇到错误:

测试方法

 class myClass implements Serializable{
    def steps
    public myClass(steps) {this.steps = steps}

    public void myMethodToTest(script, credentialsId, dataObject) {
    dataObject.myKeyValue.each {
        steps.withCredentials([[
           $class: ‘UsernamePasswordMultiBinding’, credentialsId: "${credentialsId}",
           usernameVariable: 'USR', passwordVariable: 'PWD']]) {
             steps.sh("git push --set-upstream origin ${it.branch}")
           }
      }
   }      
}

嘲讽

class Steps {
   def withCredentials(List args, Closure closure) {}
}

class Script {
    public Map env = [:]
}

测试用例

def "testMyMethod"(){
        given:
        def steps = Spy(Steps)
        def script = Mock(Script)
        def myClassObj = new myClass(steps)
        def myDataObject = [
          'myKeyValue' : [['branch' :'mock' ]]
        ]

        when:
        def result = myClassObj.myMethodToTest(script, credId, myDataObject)

        then:
        1 * steps.withCredentials([[
            $class: 'UsernamePasswordMultiBinding', 
            credentialsId: "mycredId", 
            usernameVariable: 'USR', 
            passwordVariable: 'PWD'
        ]])  
        1 * steps.sh(shString)

        where:
        credId     | shString
        "mycredId" | "git push --set-upstream origin mock"

错误(它的变量在闭包中变为空)

java.lang.NullPointerException: Cannot get property 'branch' on null object

【问题讨论】:

  • 这几乎是 this question 的副本,我已经一个多月来回答了。代码是准相同的,错误消息“调用太少”也是。您声称看到的 NPE 不会发生。您甚至没有将我的修复合并到您的示例代码中。如果您不能提出问题的可重现版本,而只是提出旧问题的副本,那么没有人可以帮助您。同样在问题标题中,您提到了一个间谍,但您的代码不包含任何内容。
  • 我同意我错过了 Spy 但这不是以前提出的问题的重复,因为以前的代码具有“它”的可变性,并且在同一问题上添加 cmets 时,您只告诉我提出新问题和当我问新问题时,您说的是重复问题。如果我能得到已经发布的问题的答案,我自己不会花时间发布新问题,因为我不是在这里增加我的否。的问题,但得到一些帮助。
  • 您只是从上一个问题中复制了原始的、未更正的伪代码,这就是为什么它是重复的。有错误的印刷双引号,测试中缺少的参数导致“调用太少”,Steps 类中缺少的方法导致另一个“调用太少”等等。所有这些我已经在我之前的答案中修复了,这也被接受了。为什么你不能只提供一个显示真正问题的工作样本而不是其他多个问题?你从来没有运行过自己的代码,否则你会注意到的。
  • 此外,Steps.withCredentials() 类从不评估闭包,我也已经修复了。如果您将您的问题基于我之前的回答,而不是再次从不完整和有错误的代码开始,那么只有这样我们才会看到您抱怨的错误消息。我刚试过。然后 Leonard 的回答解决了您的应用程序类中的问题,但不是您的示例代码中的所有其他问题。我将发布一个工作示例。

标签: groovy nullpointerexception spock spy


【解决方案1】:

你有两个嵌套闭包的情况

dataObject.myKeyValue.each { // <- first closure it references the map
    steps.withCredentials([[
       $class: ‘UsernamePasswordMultiBinding’, credentialsId: "${credentialsId}",
       usernameVariable: 'USR', passwordVariable: 'PWD']]) { // <- second closure it is null as no parameter is passed to this closure
         steps.sh("git push --set-upstream origin ${it.branch}")
    }
}

要修复它,您应该命名第一个参数

dataObject.myKeyValue.each { conf ->
    steps.withCredentials([[
       $class: ‘UsernamePasswordMultiBinding’, credentialsId: "${credentialsId}",
       usernameVariable: 'USR', passwordVariable: 'PWD']]) {
         steps.sh("git push --set-upstream origin ${conf.branch}")
    }
}

【讨论】:

  • 我仍然不知道在 Spy 的情况下嵌套闭包如何改变“它”的行为(或者通常用于测试用例而不是在生产中执行实际代码)?我试图理解这一点,因为我不太倾向于更改实际代码和可能发生的类似行为。
  • stackoverflow.com/questions/2361650/… 它与 Spock/Spy 的东西没有任何关系,但这是正常的 groovy 行为。
【解决方案2】:

请接受 Leonard 的回答,但我想发布带有少量修复的 MCVE,以便其他人可以实际运行测试并验证解决方案,因为即使有他的回答,您的代码也永远不会在没有错误的情况下运行。所以我们开始(请注意我的内联 cmets):

package de.scrum_master.stackoverflow.q60044097

class Script {
  public Map env = [:]
}
package de.scrum_master.stackoverflow.q59442086

class Steps {
  def withCredentials(List args, Closure closure) {
    println "withCredentials: $args, " + closure
    // Evaluate closure so as to do something meaningful
    closure()
  }

  // Add missing method to avoid "too few invocations" in test
  def sh(String script) {
    println "sh: $script"
  }
}
package de.scrum_master.stackoverflow.q60044097

class MyClass implements Serializable {
  def steps

  MyClass(steps) { this.steps = steps }

  void myMethodToTest(script, credentialsId, dataObject) {
    // Fix wrong quotes in ‘UsernamePasswordMultiBinding’
    // and incorporate Leonard's solution to the nested closure problem
    dataObject.myKeyValue.each { conf ->
      steps.withCredentials(
        [
          [
            $class          : 'UsernamePasswordMultiBinding',
            credentialsId   : "${credentialsId}",
            usernameVariable: 'USR',
            passwordVariable: 'PWD'
          ]
        ]
      ) {
        steps.sh("git push --set-upstream origin ${conf.branch}")
      }
    }
  }
}
package de.scrum_master.stackoverflow.q60044097

import spock.lang.Specification

class MyClassTest extends Specification {
  def "testMyMethod"() {
    given:
    def steps = Spy(Steps)
    // Actually this noes not need to be a mock, given your sample code.
    // Maybe the real code is different.
    def script = Mock(Script)
    def myClassObj = new MyClass(steps)
    def myDataObject = [
      'myKeyValue': [['branch': 'mock']]
    ]

    when:
    // Result is never used, actually no need to assign anything
    def result = myClassObj.myMethodToTest(script, credId, myDataObject)

    then:
    1 * steps.withCredentials(
      [
        [
          $class          : 'UsernamePasswordMultiBinding',
          credentialsId   : "mycredId",
          usernameVariable: 'USR',
          passwordVariable: 'PWD'
        ]
      ],
      // Add missing closure parameter placeholder '_' to make the test run
      _
    )
    1 * steps.sh(shString)

    where:
    credId     | shString
    "mycredId" | "git push --set-upstream origin mock"
  }
}

请注意:让测试运行并让应用程序做一些有意义的事情只是为了完成图片。但实际上您要问的问题是应用程序代码中的一个错误(以不正确的方式使用嵌套闭包)。测试和应用程序代码中的其他错误只是隐藏它,因为测试甚至从未到达有问题的部分。


更新:您的问题归结为这包括两个可能的解决方案(B 基本上是 Leonard 建议的):

def evalClosure(Closure closure) {
  closure()
}

// Problem: inner closure's 'it' shadowing outer closure's 'it'
[1, 2].each {
  println "outer closure: it = $it"
  evalClosure {
    println "inner closure: it = $it"
  }
}

println "-" * 30

// Fix A: make inner closure explicitly parameter-less
[1, 2].each {
  println "outer closure: it = $it"
  evalClosure { ->
    println "inner closure: it = $it"
  }
}

println "-" * 30

// Fix B: explicitly rename outer closure's parameter
[1, 2].each { number ->
  println "outer closure: number = $number"
  evalClosure {
    println "inner closure: it = $it"
    println "inner closure: number = $number"
  }
}

控制台日志:

outer closure: it = 1
inner closure: it = null
outer closure: it = 2
inner closure: it = null
------------------------------
outer closure: it = 1
inner closure: it = 1
outer closure: it = 2
inner closure: it = 2
------------------------------
outer closure: number = 1
inner closure: it = null
inner closure: number = 1
outer closure: number = 2
inner closure: it = null
inner closure: number = 2

【讨论】:

  • 请注意我的更新,支持 Leonard 对您的应用程序代码出了什么问题的解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-16
  • 1970-01-01
相关资源
最近更新 更多