groovy тестирование спока с помощью шпиона - PullRequest
0 голосов
/ 03 февраля 2020

У меня есть общая библиотека, которая вызывает метод шага конвейера (withCredentials). Я пытаюсь проверить, правильно ли вызывается метод withCredentials с помощью sh сценариев при вызове myMethodToTest, но возникает ошибка при повторении в замыкании 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

Ответы [ 2 ]

2 голосов
/ 04 февраля 2020

У вас есть случай двух вложенных замыканий

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}")
    }
}
0 голосов
/ 05 февраля 2020

Пожалуйста, примите ответ Леонарда, но я хочу опубликовать MCVE с несколькими исправлениями, чтобы другие люди могли фактически запустить тест и проверить решение, потому что даже с его ответом ваш код никогда не будет работать без ошибки. Итак, здесь мы go (обратите внимание на мои встроенные комментарии):

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

Обратите внимание: Выполнение теста и запуск приложения что-то незначительно значимое просто предназначено для завершения картины. Но на самом деле проблема, о которой вы спрашивали, - это ошибка в коде вашего приложения (неправильное использование вложенных замыканий). Другие ошибки как в тестовом, так и в коде приложения просто скрывали ее потому что тест даже не достиг проблематичной c части.


Обновление: Ваша проблема сводится к следующему, включая два возможных решения (в основном то, что предложил Леонард):

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
...