Jenkins: Testing conditional logic for stages in your pipeline

Some steps in your development or release process can only be executed when the conditions are right. An example of this is that releases to Production can only be done from the production branch, or that deployments to Acceptance can only occur when they are approved by a specific user.
This is where conditions shine, and they are implemented in Jenkins Pipelines as a when block.

This post is part of a Series in which I elaborate on my best practices for running Jenkins at scale which might benefit Agile teams and CI/CD efforts.

Whenever there’s logic involved in your code, one has to be extra careful that the behaviour of this code is both correct and consistent over time. Tests help with both.

Now, let’s take a look at our pipeline for this blog post.

Our pipeline

Again, we are working with the Pipeline from the previous posts, but this time we have to alter it slightly to use the when directive.
This is what we have now:

pipeline {
    agent none
 
    stages {
        stage('Build and Unit test') {
            agent { label 'maven' }
            steps {
                script {
                    module_Maven('clean verify')
                }
            }
            post {
                always {
                    junit testResults: '**/target/surefire-reports/*.xml', allowEmptyResults: false
                }
            }
        }
        stage('Publish to Nexus') {
            agent { label 'maven' }
            when {
                beforeAgent true
                branch 'master'
            }
            steps {
                script {
                    echo "This is where we publish to Nexus"
                    module_Artifact.publish()
                }
            }
        }
    }
    post {
        always {
            script {
                module_Notification.sendEmail(currentBuild.result)
            }
        }
    }
}

The close reader will have spotted the added code:

when {
    beforeAgent true
    branch 'master'
}

This piece of Pipeline code instructs Jenkins to check whether the branch this build runs on is the ‘master’ branch. Jenkins should also check this before a suitable agent is found, to avoid unnecessary usage of agents and slower builds.
Please also note that this condition could have been written in plain Groovy as:

when {
    beforeAgent true
    expression { BRANCH_NAME ==~ /(master)/ }
}

The documentation states the following about the when block:

The when directive allows the Pipeline to determine whether the stage should be executed depending on the given condition.

This means we can get optional execution of stages based on certain conditions:

Condition What does it check?
branch When branch matches the given pattern
buildingTag When the build is building a tag
changelog When SCM changelog contains a given regular expression
changeset When SCM changeset contains a file matching the given string/glob
changeRequest When the build is for a “change request” (Pull Request on GitHub/Bitbucket or Merge Request on GitLab)
environment When an environment variable is set to a specified value
equals When the expected value is equal to the actual value
expression When specified Groovy expression evaluates to true
tag When this build is building a tag matching the specified string/glob
triggeredBy When the build is triggered by the given parameter
beforeAgent Evaluate when before entering agent in a stage
beforeInput Evaluate when before the input directive

And there even are some logic operators:

Condition
not When the nested condition is false
allOf When all of the nested conditions are true
anyOf When at least one of the nested conditions is true

With these boolean logic operators, we would be able to create very intricate condition statements if required. However, it can be considered best practice to at least keep the conditions readable and concise. I would recommend using one or two conditions at best, to keep the pipeline easily understandable for all users.

Testing the condition

The test frameworks we have selected (Jenkins PipelineUnit, Spock and pipelineUnit) support the majority of these conditions, but it is still incomplete. It will, however, correctly interpret the boolean conditions.

Branches

We are testing a condition which checks for the name of the branch the build is running on. It is very handily defaulted to ‘master’ in the test framework, but it can also be set in your test.

A test for the above snippet of the pipeline could look something like this:

void 'Happy flow'() {
    when:
    script.call([:])

    then:
    1 * mavenMock.call('clean verify')
    1 * artifactMock.publish()
    1 * notificationMock.sendEmail(_)
    assertJobStatusSuccess()
}

As everything is as it should be, the test simply completes with a success (as the branch defaults to ‘master’ in the framework).
A new scenario where we actively set the branch to be something different would look like this:

void 'When not on master, do not publish to Nexus'() {
    given:
    binding.setVariable('BRANCH_NAME', 'development')

    when:
    script.call([:])

    then:
    1 * mavenMock.call('clean verify')
    0 * artifactMock.publish()
    assertJobStatusSuccess()
}

This code sets the variable BRANCH_NAME to development and runs the pipeline.
After this, it checks for a call to Maven but does not expect a call to publish the artefact in Nexus (as it would be skipped due to the conditional). The pipeline should complete successfully, as the condition merely instructs Jenkins to skip the stage but not fail on this.

The call stack would look like this:

post changed skipped as not CHANGED
Stage 'Publish to Nexus' skipped due to when branch is false
>>>>>> pipeline call stack -------------------------------------------------
   defaultPipeline.call({})
      defaultPipeline.pipeline(groovy.lang.Closure)
         defaultPipeline.agent(groovy.lang.Closure)
         defaultPipeline.stages(groovy.lang.Closure)
            defaultPipeline.stage(Build and Unit test, groovy.lang.Closure)
               defaultPipeline.agent(groovy.lang.Closure)
                  defaultPipeline.label(maven)
               defaultPipeline.steps(groovy.lang.Closure)
                  defaultPipeline.script(groovy.lang.Closure)
                     defaultPipeline.module_Maven(clean verify)
               defaultPipeline.post(groovy.lang.Closure)
                  defaultPipeline.always(groovy.lang.Closure)
                     defaultPipeline.junit({testResults=**/target/surefire-reports/*.xml, allowEmptyResults=false})
                  defaultPipeline.changed(groovy.lang.Closure)
            defaultPipeline.stage(Publish to Nexus, groovy.lang.Closure)
               defaultPipeline.agent(groovy.lang.Closure)
                  defaultPipeline.label(maven)
               defaultPipeline.when(groovy.lang.Closure)
                  defaultPipeline.beforeAgent(true)
                  defaultPipeline.branch(master)
         defaultPipeline.post(groovy.lang.Closure)
            defaultPipeline.always(groovy.lang.Closure)
               defaultPipeline.script(groovy.lang.Closure)

Please note the two lines at the top: the post section is skipped as the status is not changed (SUCCESS before and SUCCESS now), and the stage ‘Publish to Nexus’ is skipped as the specified condition evaluated to false.
Great, this is what we need!

Tags

Jenkins helps you wuite a lot when it comes to building from a tag, as it handily provides an environment variable to that build by the name of TAG_NAME which has the value of that specific tag.
Therefore it is quite easy to influence this in your test: you just have to set the variable TAG_NAME to something, and the test framework will work off of that if needed. The test frameworks do not set the value beforehand, so the author of the test is responsible for a correct value.

You’d actually be tempted to set the variable BRANCH_NAME to a specific value containing the word tag, but then you just made a wrong assumption about the inner workings of Jenkins. ????

A Pipeline snippet considering a build running on a tag can be something like this:

when {
    buildingTag()
}

or

when {
    tag "release-*"
}

These snippets would both check whether the build runs on a tag, and the second snippet even checks if the tag is according to the glob-style matcher it provides. Functionally, it would evaluate to true and then run the correct stage when the build is running on a tag for a release.

The test would be:

void 'When building a release tag, publish to Nexus'() {
    given:
    binding.setVariable('TAG_NAME', 'release-1.1.0')

    when:
    script.call([:])

    then:
    1 * mavenMock.call('clean verify')
    1 * artifactMock.publish()
    assertJobStatusSuccess()
}

This test first sets the variable TAG_NAME to a value indicating a release tag (for version 1.1.0) and runs the pipeline. It then assumes that we do a call to Maven and publish to Nexus without any failures.
Please note that setting the branch name (BRANCH_NAME) is not required and actually will not do anything for this example as Jenkins works differently.

Boolean operators

To negate the condition you are testing for, you can use the not condition.
An example would be the situation where you want to limit a stage to not be executed when a condition is true. For example: you don’t want the artefacts from a feature branch to be stored in Nexus.
In this example, I’m assuming a strict naming convention for feature branches in which they are prepended with feature/.
Examples:

  • feature/migrate-to-newer-version-of-maven
  • feature/add-new-instructions-for-contributors

Your pipeline would look like this:

when {
    not {
        branch 'feature/*'
    }
}
...

The test would need to set the name of the branch in the given clause:

void 'When building a feature branch, do not publish to Nexus'() {
    given:
    binding.setVariable('BRANCH_NAME', 'feature/this-is-my-best-feature-yet')

    when:
    script.call([:])

    then:
    1 * mavenMock.call('clean verify')
    0 * artifactMock.publish()
    assertJobStatusSuccess()
}

Conclusion

Much like the post conditions which I covered in a previous post, the when conditions enable you to write Pipelines with logic in them. The when conditions work before the execution of a stage on certain values in your build environment (like branch names, environment values and even commit messages) and the post conditions work with the outcome of the steps in a stage after they’ve been executed.
Our test frameworks will happily accommodate you using any of those or a combination of them all, and I hope I’ve been able to show you several often used variations of them and how to set it up in your tests.