Search code examples
jenkinsgroovymockingjenkins-pipelinespock

Groovy shared library testing pipeline step method with Spock


I have shared library that calls pipeline step method(withCredentials).I am trying to test withCredentails method is being called correctly with sh scripts on calling myMethodToTest but facing error:

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

    public void myMethodToTest(script, String credentialsId) {
        steps.withCredentials([[$class: ‘UsernamePasswordMultiBinding’, credentialsId: "${credentialsId}", usernameVariable: ‘USR’, passwordVariable: ‘PWD’]]) {
             steps.sh """
                export USR=${script.USR}
                export PWD=${script.PWD}
                $mvn -X clean deploy
             """
          }
     }
}

//Mocking

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

class Script {
    public Map env = [:]
}

//Test case

def "testMyMethod"(){
        given:
        def steps = Mock(Steps)
        def script = Mock(Script)
        def myClassObj = new myClass(steps)
        script.env['USR'] = "test-user"

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

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

        where:
        credId | shString
        "mycredId" | "export USR='test-user'"

//Error

Too few invocations for:

1 * steps.withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: "mycredId", usernameVariable: ‘USR’, passwordVariable: ‘PWD’]])   (0 invocations)

Unmatched invocations (ordered by similarity):

1 * steps.withCredentials([['$class':'UsernamePasswordMultiBinding', 'credentialsId':mycredId, 'usernameVariable’:’USR’, 'passwordVariable':'PWD’]]

Solution

  • You have a whole bunch of subtle and not so subtle errors in your code, both test and application classes. So let me provide a new MCVE in which I fixed everything and commented a few crucial parts inside the test:

    package de.scrum_master.stackoverflow.q59442086
    
    class Script {
      public Map env = [:]
    }
    
    package de.scrum_master.stackoverflow.q59442086
    
    class Steps {
      def withCredentials(List args, Closure closure) {
        println "withCredentials: $args, " + closure
        closure()
      }
    
      def sh(String script) {
        println "sh: $script"
      }
    }
    
    package de.scrum_master.stackoverflow.q59442086
    
    class MyClass implements Serializable {
      Steps steps
      String mvn = "/my/path/mvn"
    
      MyClass(steps) {
        this.steps = steps
      }
    
      void myMethodToTest(script, String credentialsId) {
        steps.withCredentials(
          [
            [
              class: "UsernamePasswordMultiBinding",
              credentialsId: "$credentialsId",
              usernameVariable: "USR",
              passwordVariable: "PWD"]
          ]
        ) {
          steps.sh """
            export USR=${script.env["USR"]}
            export PWD=${script.env["PWD"]}
            $mvn -X clean deploy
          """.stripIndent()
        }
      }
    }
    
    package de.scrum_master.stackoverflow.q59442086
    
    import spock.lang.Specification
    
    class MyClassTest extends Specification {
      def "testMyMethod"() {
        given:
        // Cannot use mock here because mock would have 'env' set to null. Furthermore,
        // we want to test the side effect of 'steps.sh()' being called from within the
        // closure, which also would not work with a mock. Thus, we need a spy.
        def steps = Spy(Steps)
        def myClass = new MyClass(steps)
        def script = new Script()
        script.env['USR'] = "test-user"
    
        when:
        myClass.myMethodToTest(script, credId)
    
        then:
        1 * steps.withCredentials(
          [
            [
              class: 'UsernamePasswordMultiBinding',
              credentialsId: credId,
              usernameVariable: 'USR',
              passwordVariable: 'PWD'
            ]
          ],
          _  // Don't forget the closure parameter!
        )
        // Here we need to test for a substring via argument constraint
        1 * steps.sh({ it.contains(shString) })
    
        where:
        credId     | shString
        "mycredId" | "export USR=test-user"
      }
    }