Write your own pipeline testing framework

Introduction

I had a need recently to start using pipeline testing framwework in order to be able to test number of pipeline files I am using on daily basis. I tell you, it is bad feeling when number of pipeline scripts is growing and the only testing you can do is to make some sandboxing inside Jenkins…
So I started to look around and at first I found this framework for testing which looked quite promising:
https://github.com/jenkinsci/JenkinsPipelineUnit
Unfortunately, almost immediately I stumbled upon problem of malfunctioning mock for local functions: I was not able to mock internal function – it was only possible to mock steps, so I would have to mock all of the atomic steps of all auxiliary functions I was using. Imagine function with 50 steps which you just need to mock but you cannot do it and the only workaround is to mock most of that 50 steps…
For me lesson learned is: open source solution is good when it is working propery or all defects can be worked around easily. Project has to be very active I guess with lot of developers involved. This is not the case here – I would guess this even may be abandoned in the future.
Anyway, because workaround was hard and I found yet another problem with that framework, I decided to try to create my own solution.

Understanding the problem

When looking at typical declarative or scripted pipeline one can see it reminds groovy. Let’s look at the example:

Trying to launch simplePipeline.groovy outside of Jenkins as regular groovy script ends up with error like this:

So, the problem with running it with standard groovy is for sure pipeline. But what does it mean in terms of syntax? Well, it is just a function named pipeline which accepts closure as parameter:

pipeline { ... } 

The other items are other groovy functions as well, they just have 2 arguments like:

stage('Second stage') { ... } 

This one is stage function which has String as first argument and closure as the second one.

It is clear now that our goal is just to mock all pipeline specific functions so that they are runnable by groovy. This will allow to run whole pipeline file outside of Jenkins ! It sounds complicated but it is not. Read on.

Making it runnable

Let’s see the example of how can we write first version of code which will run pipeline example:

If you run it you will see it works now! All jenkins related syntax items are mocked and so groovy can execute the file. How does it work?

I use expando class mechanism here which is part of runtime metaprogramming in groovy (You can read about it here:

https://www.groovy-lang.org/metaprogramming.html#metaprogramming_emc).

In highlighted lines you can observe how “pipeline” function is mocked. It says: whenever you meet function named pipeline with matching signature (which is array of one or more objects), run the assigned closure (run the code in curly braces).

So, during the runtime the code is run and it means that the closure:

  • logs word “pipeline”
  • treats first parameter as a closure and runs it (which means in this example that “agent” function will be run and so next mock will be executed)

This is the core of the pipeline testing framework: mocking jenkins syntax which is expressed in pipeline file in curly braces as closures.

Of course not all of the syntax items can be mocked this way, as for example there are functions like echo which have String parameter only. Their mock is just printing it out.

Improving the solution

The first version contains much redundancy. We can do some improvements here:

It looks much better, we separated steps and sections and only one piece of code is responsible for mocking each of them. We can also easily add new mocks now. We even have here this crazy feature of dynamic method names. It is awesome!

However, we still cannot do any assertions. Let’s solve this problem as well. Firstly, we need to find a way to trace the script during the runtime and store its states in some memory structure. We need to know for each step in the pipeline:

  1. the caller hierarchy (which syntax item called which one, “did stage 1 called echo ?” )
  2. what was the state of global variables (“was env.TEST_GLOBAL_VAR set in “First stage” and did it have value “global_value” in the “Second stage”?)
  3. what exactly was called (“was echo step called with “test1″ parameter ?”)

Ad. 1 We can achieve by extracting information from stack trace using for example these 2 methods:

Numbers of lines from pipeline file are caught and translated into text – we know what is the caller hirarchy now.

Ad. 2 We can get information about state of global variables by using: pipelineScript.getBinding().getVariables()

when mocking.

Ad. 3 To get exact syntax item with parameters we can report them during mocking as well.

This is example for 2 and 3:

Method “storeInvocation” is called with 3 parameters:

  • syntax item name
  • its parameters
  • current state of variables

In this method we have 2, 3 coming as parameters, and 1 created by “createStackLine” function. I store it in ResultStackEntry class and name it: 1-stackLine, 2-invocations and 3-runtimeVariables.

Take a look on github for details.

Putting it all together, this code:

produces stack trace in the output:

The last important thing here is global environment handling. In Jenkins, there is environment map named ENV where keys can be set by assigning values like this: env.TEST_VARIABLE="test1"

Groovy by default doesn’t know anything of such map, so it is neccesary to prepare it so that pipeline can populate it during runtime:

Assertions

We have working framework with result stack but we still cannot do any assertions. We have to add this ability to be able to test anything, don’t we ?

Let’s add some result stack validator (look at the Github for full code):

And assertion class to be mini DSL for asserting result stack when writing tests:

Only now can we start doing some assertions:

E voila. So finally we have very simple working example which can run simple pipeline and assert simple things.

We can use as starting point for real framework with mocking all aspects of pipeline file: sections, steps, functions, objects and properties.
This is all possible with using just 2 aspects of metaprogramming in Groovy: expando metaclasses and binding.

Full code:

https://github.com/grzegorzgrzegorz/pipeline-testing/tree/master

Interesting links:
https://www.groovy-lang.org/metaprogramming.html
http://events17.linuxfoundation.org/sites/events/files/slides/MakeTestingGroovy_PaulKing_Nov2016.pdf

Leave a Reply

Your email address will not be published. Required fields are marked *

*