jenkinsci / JenkinsPipelineUnit

Framework for unit testing Jenkins pipelines
MIT License
1.54k stars 394 forks source link

Feature request: mock global var methods #138

Open thorntonrose opened 5 years ago

thorntonrose commented 5 years ago

Please add the ability to mock methods of global vars with registerMethod. For example, given vars/foo.groovy:

def call() { ... }
def bar() { ... }

Provide a way to mock foo.bar().

mblanloeil commented 5 years ago

Hi, Have you a solution for this issue ?

Thank's

franknarf8 commented 5 years ago

Actually,

You can mock bar, but only if it the calling scope is within a different file. (using registerAllowedMethod('bar',[], null).

It doesn't work when you call the function within the same file as they are contained within the same class and the metaClass.invokeMethod nor the metaClass.static.invokeMethod functions are called when calling a different function from the same class. (I think it might be related to initializing PogoMetaMethodSite vs PogoMetaClassSite somewhere deep in the groovy core)

I have no clue how to fix this, but it would be very nice to be able to mock these functions.

Thanks

idij commented 5 years ago

You can use an Expando as a workaround for this. Something like any of these:

binding.setVariable('bar', new Expando (foo: "foo result")
binding.setVariable('bar', new Expando (foo: "foo result", baz: "baz result")
binding.setVariable('bar', new Expando (foo: { return calculate_foo_result () })
etc.
franknarf8 commented 5 years ago

I think my explanation might have been a bit confusing, I'll retry to explain with an example:

scriptA.groovy

call(Map args) {
    print()
}

void print() {
    echo('hello')
}

scriptB.groovy

call(Map args) {
    scriptA.echo('hello')
}

When testing scriptB, I'm doing something of the sort, and I am able to successfully mock print

binding.setVariable('scriptA', loadScript('vars/scriptA.groovy'))
helper.registerAllowedMethod('print', []) { -> } // <- mock gets called successfully
loadScript('vars/scriptB.groovy').call()

But, when testing scriptA, it is not possible to mock print

helper.registerAllowedMethod('print', []) { -> } // <- mock never gets called
loadScript('vars/scriptA.groovy').call()
stchar commented 4 years ago

Duplicates #51 #142 #141

zionyx commented 4 years ago

helper.registerAllowedMethod('print', []) { -> } // <- mock never gets called

Try

    helper.registerAllowedMethod('print', [], { null })

I mocked a lot of global methods and worked pretty nicely. I tested the above approach and worked as aspected. You can even add some codes in the null section and put breakpoints into it to verify.

    helper.registerAllowedMethod('print', [], {
        println 'Put a breakpoint here and see it breaks.'
    })
jgsogo commented 4 years ago

I'm trying to make something like this work, but without success so far... I have a shared library with several vars/*****.groovy files exposing different functions. I'm using them from my Jenkinsfile using xxxxx.function(...) and yyyyy.function(...) where xxxxx and yyyyy are the names of those vars/*****.groovy files.

I need to mock all the functions in one of the *****.groovy files as they are making calls to externals systems that are not available while testing. Something like helper.registerAllowedMethod('xxxxx.function', ....) would be perfect, but it doesn¡t work, and I'm not able to find an alternative using other mocking frameworks as I don't know how to refer to the class I need to mock.

Any hint would be appreciated. Thanks!

zionyx commented 4 years ago

helper.registerAllowedMethod('xxxxx.function', ....) won't work because it only registers a single top level method. For your use case, you must mock the class xxxxx.

You can then set class xxxxx as property xxxxx so your shared library can use them in the tests as xxxxx.function() calls.

        binding.setProperty('xxxxx', new xxxxx())

An example of a mocked class:

package mock // up to you

class PipelineHelper implements Serializable {

    public void requiredParameterCheck(def paramName, def paramValue, def errorMessage = '') {
        if (errorMessage == '') {
            errorMessage = "--- SHARED LIBRARY ERROR: Missing required parameter ${paramName} ---"
        }
        if ((paramValue == null) || (paramValue == '')) {
            throw new Exception(errorMessage)
        }
    }
}

This mocked class is a partial copy of my pipelineHelper.groovy. The requiredParameterCheck() method is commonly used in other shared pipeline libraries. Notice that i do not mock def call() {...} of pipelineHelper.groovy because it is not in the scope for mocking.

Depending on the complexity of the method. If you use steps like echo in the method above, you need to register echo in your test file with:

        helper.registerAllowedMethod('echo', [String.class], {
            null // or do something with it
        })

My shared pipeline library is unfortunately not open source yet, but I work towards open sourcing it. Hope that helps.