Injecting custom feature method parameters does not work nicely with data-driven methods #652

Open
Vampire opened this Issue Sep 26, 2016 · 1 comment

Projects

None yet

1 participant

@Vampire
Contributor
Vampire commented Sep 26, 2016

Having the following global extension

public class StringInjectorExtension implements IGlobalExtension {
    @Override
    public void start() {
    }

    @Override
    public void visitSpec(SpecInfo spec) {
        spec.allFeatures.featureMethod*.addInterceptor(new IMethodInterceptor() {
            @Override
            void intercept(IMethodInvocation invocation) throws Throwable {
                def parameterCount = invocation.method.reflection.parameterCount
                if (parameterCount > invocation.arguments.length) {
                    def newArguments = new Object[parameterCount]
                    System.arraycopy invocation.arguments, 0, newArguments, 0, invocation.arguments.length
                    invocation.arguments = newArguments
                }
                invocation.method.reflection.parameterTypes.eachWithIndex{ type, i ->
                    if (String.equals(type)) {
                        invocation.arguments[i] = 'foo: ' + System.currentTimeMillis()
                    }
                }
                invocation.proceed()
            }
        })
    }

    @Override
    public void stop() {
    }
}

and the specification

class TestSpecification extends Specification {
    // does not compile because of
    // "Data variable 'a' needs to be declared as method parameter"
    // and
    // "Data variable 'b' needs to be declared as method parameter"
    // and
    // "Parameter 'string' does not refer to a data variable"
    @Unroll
    def 'test1 | a = #a | b = #b'(String string) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
    }

    // does not compile because of
    // "Parameter 'string' does not refer to a data variable"
    @Unroll
    def 'test2 | a = #a | b = #b'(String string, def a, def b) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
    }

    // does not compile because of
    // "Parameter 'string' does not refer to a data variable"
    @Unroll
    def 'test3 | a = #a | b = #b'(def a, def b, String string) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
    }

    // does not compile because of
    // "Parameter 'string' does not refer to a data variable"
    @Unroll
    def 'test4 | a = #a | b = #b'(String string, def b, def a) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
    }

    // does not compile because of
    // "Parameter 'string' does not refer to a data variable"
    @Unroll
    def 'test5 | a = #a | b = #b'(def a, def b, String string) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
    }

    @Unroll
    def 'test6 | a = #a | b = #b'(def a, def b, String string) {
        given:
        println "string = $string"
        println "a = $a"
        println "b = $b"

        expect:
        string?.startsWith('foo')
        a in ['a1', 'a2']
        b in ['b1', 'b2']

        where:
        a << ['a1', 'a2']
        b << ['b1', 'b2']
        string = null
    }

    def 'test7'(String string) {
        given:
        println "string = $string"

        expect:
        string?.startsWith('foo')
    }
}

only test6 and test7 compile and run as expected.
test1 to test5 do not compile with the reasons given in the inline comment above.

This means if you want to inject custom feature method parameters (e. g. when using JMockit because of the need of mocking static calls or mocking calls in objects that are not create by the test (given JMockit would support Spock which it does not yet, but I might try to contribute this if it would work properly), or when injecting custom stuff for test (I actually tried this for a project when I found this issue)) into data-driven methods, you always have to

  • declare all data variables as parameters
  • declare the custom injected parameter as data variable (value doesn't matter as the custom injection happens after spock internal data variable injection)

Both points seems a bit cumbersome to use and superfluous imho.

What I would like to have as behaviour is:

  • If a data variable is declared via where: block
    • if the data variable is declared as method parameter (matched via name), inject the data variable into that method parameter
    • if the data variable is not declared as method parameter (matched via name), inject the data variable into an automatically added method parameter
  • if there are method parameters that are not declared as data variables, do not fail the compilation but inject null, or the datatype default value (null for objects, 0 for numerics, false for booleans, ...), expecting that some extension injects the correct value for execution
@Vampire
Contributor
Vampire commented Sep 27, 2016

Also I suggest having invocation.arguments always as long as the method has parameters, so that a custom parameter providing interceptor can simple set the according array field without the need to check array length and create a new one first if not long enough.

Or of course some other means to set parameters from interceptors like by name or similar.

@Vampire Vampire referenced this issue in jmockit/jmockit1 Oct 26, 2016
Closed

Spock and Groovy Support #358

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment