ExpediaGroup / jenkins-spock

Unit-test Jenkins pipeline code with Spock
https://javadoc.io/doc/com.homeaway.devtools.jenkins/jenkins-spock
Apache License 2.0
187 stars 76 forks source link

Global variables method in tested script cannot be mocked #96

Closed PavelKa closed 3 years ago

PavelKa commented 3 years ago

Desired Behavior

If multiple methods are defined in vars. e.g. :

//vars/var1.groovy 
def callMocked(){
     toMock()
}

def toMock() {
    "not mocked"
}

then it would be useful to have the possibility to easy mock methods in the tested script.. following test will fail because var1.toMock is not mocked :

     def xVar1 = null

    def setup() {
        xVar1 = loadPipelineScriptForTest("vars/var1.groovy")
        getPipelineMock("var1.toMock")() >>{ "mocked2" }
    }

    def "test method toMOck should be mocked "() {
        expect:
         "mocked2" == xVar1.callMocked()
    }

Mocking of methods in scripts that are not tested/ loaded using loadPipelineScriptForTest works as expected and can be mocked using getPipelineMock e.g.:

    ...
    xVar1 = loadPipelineScriptForTest("vars/var1.groovy")
        getPipelineMock("var2.toMock")() >>{ "mocked" }
...

Benefits

  1. Easy testing of vars containing multiple global methods
deblaci commented 3 years ago

Hi,

You should not mock the class under testing and var1 is that. Rather mock the external function calls in callMocked().Hi, You schould not mock the class under test.

PavelKa commented 3 years ago

You should not mock the class under testing and var1 is that. Rather mock the external function calls in callMocked().

Hi, the example is very simplified. I need to create a test for legacy implementation where many global methods in one file (var1) exist. These global methods call each other so it would be very useful to mock also methods in the tested class (var1) and have the possibility to test that mocked method was called with expected parameters values in the same way as getPipelineMock does. As a workaround, I rewrite the static method in the test method and add a test to this mock, although getPipelineMock would be more straightforward.

//vars/var1.groovy 
def callMocked(){
     toMock("XXX")
}

def toMock( x ) {
    "not mocked"
}

Mocking method in the same class:

xVar1.metaClass.static.toMock << { x->
            assert "XXX" == x
            "mocked2"
        }
awittha commented 3 years ago

That's a tough situation.

You say "legacy" - does this mean you are unable to make changes to the source code of the project that contains var1? That would be the most-correct, best, and easiest place to write tests for the functionality in var1. It will be much harder (especially if the methods call each other) to test var1 from outside that project.

Arguably, that's not a problem that unit-test frameworks are even charged with solving.

You need a Spock Spy to be able to stub/mock individual methods on a real object, to enable testing of a single method (that calls other methods on the same object) in isolation.

However, to load the "random" groovy Script into something that can call Jenkins pipeline steps, you need loadPipelineScriptForTest which ... creates its own special implementation (that is not a spy) to enable the above.

In your case, the metaClass method you have found is the best way to unit-test individual methods on a class outside your project, where those methods call each other. (You're essentially manually spying on the PipelineVariableImpersonator object that jenkins-spock creates to represent var1.groovy).

The problem isn't fundamentally a problem for jenkins-spock, but for the unit-testing framework (vanilla spock) in general.