简体   繁体   English

groovy spock 使用 spy 测试闭合

[英]groovy spock testing closure with spy

I have shared library that calls pipeline step method(withCredentials).I am trying to test withCredentials method is being called correctly with sh scripts on calling myMethodToTest but facing error while iterating in withCredentials closure:我有调用管道步骤方法(withCredentials)的共享库。我试图测试 withCredentials 方法是否在调用 myMethodToTest 时使用 sh 脚本正确调用,但在迭代 withCredentials 闭包时遇到错误:

Method To Test测试方法

 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}")
           }
      }
   }      
}

Mocking嘲讽

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

class Script {
    public Map env = [:]
}

Test case测试用例

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"

Error (it variable becomes null in closure)错误(它变量在闭包中变为空)

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

You have a case of two nested closures你有两个嵌套闭包的情况

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}")
    }
}

To fix it you should name the first parameter要修复它,您应该命名第一个参数

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

Please accept Leonard's answer, but I want to post an MCVE with a handful of fixes so other people can actually run the test and verify the solution, because even with his answer your code will never run without errors.请接受伦纳德的回答,但我想发布一个带有一些修复的MCVE ,这样其他人就可以实际运行测试并验证解决方案,因为即使有他的回答,您的代码也永远不会运行没有错误。 So here we go (please note my inline comments):所以我们开始了(请注意我的内嵌评论):

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"
  }
}

Please note: Making the test run and making the application do something marginally meaningful is just meant to complete the picture.请注意:让测试运行并让应用程序做一些有意义的事情只是为了完成图片。 But actually the problem you were asking about is a bug in your application code (using nested closures in an incorrect way).但实际上您要问的问题是您的应用程序代码中的错误(以不正确的方式使用嵌套闭包)。 The other bugs in both test and application code were just hiding it because the test never even reached the problematic part.测试和应用程序代码中的其他错误只是隐藏了它,因为测试甚至从未到达有问题的部分。


Update: Your problem boils down to this including two possible solutions (B basically being what Leonard suggested):更新:您的问题归结为包括两种可能的解决方案(B 基本上是伦纳德建议的):

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"
  }
}

Console log:控制台日志:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM