Rusty Shackleford
Rusty Shackleford

Reputation: 1141

Spock: How to mock a method that accepts a single byte[] parameter?

I'm having a problem getting Spock to mock a method that accepts a single byte[] as a parameter.

A simple toy example that is failing in an identical manner to my production code follows:

import java.util.function.Consumer
import spock.lang.Specification

class ConsumerSpec extends Specification {
    // ... elided ...

    def '4: parameter is of an array type using single typed argument'() {
        given:
        def consumer = Mock(Consumer)

        when:
        consumer.accept([20, 21] as byte[])

        then:
        consumer.accept(_) >> { byte[] arg ->
            assert arg[0] == 20
            assert arg[1] == 21
        }
    }

    // ... elided ...
}

The failure message is

ConsumerSpec > 4: parameter is of an array type using single typed argument FAILED
    org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '[[B@43562196]' with class 'java.util.Arrays$ArrayList' to class 'java.lang.Byte'
        at groovy.lang.Closure.call(Closure.java:423)
        at org.spockframework.mock.response.CodeResponseGenerator.invokeClosure(CodeResponseGenerator.java:53)
        at org.spockframework.mock.response.CodeResponseGenerator.doRespond(CodeResponseGenerator.java:36)
        at org.spockframework.mock.response.SingleResponseGenerator.respond(SingleResponseGenerator.java:31)
        at org.spockframework.mock.response.ResponseGeneratorChain.respond(ResponseGeneratorChain.java:45)
        at org.spockframework.mock.runtime.MockInteraction.accept(MockInteraction.java:76)
        at org.spockframework.mock.runtime.MockInteractionDecorator.accept(MockInteractionDecorator.java:46)
        at org.spockframework.mock.runtime.InteractionScope$1.accept(InteractionScope.java:41)
        at org.spockframework.mock.runtime.MockController.handle(MockController.java:39)
        at org.spockframework.mock.runtime.JavaMockInterceptor.intercept(JavaMockInterceptor.java:72)
        at org.spockframework.mock.runtime.DynamicProxyMockInterceptorAdapter.invoke(DynamicProxyMockInterceptorAdapter.java:28)
        at ConsumerSpec.4: parameter is of an array type using single typed argument(ConsumerSpec.groovy:52)

I was relying on the behavior described in the Spock docs for Computing Return Values under Interaction Based Testing. I've selectively transcribed the relevant bits below:

If the closure declares a single untyped parameter, it gets passed the method’s argument list... If the closure declares more than one parameter or a single typed parameter, method arguments will be mapped one-by-one to closure parameters...

I added a few other tests to the above spec to see if I understood these statements. The complete MCVE follows:

$ gradle --version

------------------------------------------------------------
Gradle 2.13
------------------------------------------------------------

Build time:   2016-04-25 04:10:10 UTC
Build number: none
Revision:     3b427b1481e46232107303c90be7b05079b05b1c

Groovy:       2.4.4
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_91 (Oracle Corporation 25.91-b14)
OS:           Linux 4.4.8-300.fc23.x86_64 amd64

// build.gradle

plugins {
  id 'groovy'
}

repositories {
  mavenCentral()
}

dependencies {
  testCompile(
    [group: 'org.spockframework', name: 'spock-core', version: '1.0-groovy-2.4']
  )
}

// src/test/groovy/ConsumerSpec.groovy

import java.util.function.Consumer
import spock.lang.Specification

class ConsumerSpec extends Specification {
    def '1: parameter is of a non-array type using single untyped argument'() {
        given:
        def consumer = Mock(Consumer)

        when:
        consumer.accept('value')

        then:
        consumer.accept(_) >> { args ->
            String arg = args[0]
            assert arg == 'value'
        }
    }

    def '2: parameter is of a non-array type using single typed argument'() {
        given:
        def consumer = Mock(Consumer)

        when:
        consumer.accept('value')

        then:
        consumer.accept(_) >> { String arg ->
            assert arg == 'value'
        }
    }

    def '3: parameter is of an array type using single untyped argument'() {
        given:
        def consumer = Mock(Consumer)

        when:
        consumer.accept([20, 21] as byte[])

        then:
        consumer.accept(_) >> { args ->
            byte[] arg = args[0]
            assert arg[0] == 20
            assert arg[1] == 21
        }
    }

    def '4: parameter is of an array type using single typed argument'() {
        given:
        def consumer = Mock(Consumer)

        when:
        consumer.accept([20, 21] as byte[])

        then:
        consumer.accept(_) >> { byte[] arg ->
            assert arg[0] == 20
            assert arg[1] == 21
        }
    }

    def '5: parameter is of an array type without using Mock'() {
        given:
        def consumer = { byte[] arg ->
            assert arg[0] == 20
            assert arg[1] == 21
        } as Consumer<byte[]>

        expect:
        consumer.accept([20, 21] as byte[])
    }
}

Once again, the only test that fails is (4).

Based on the failure message, it's almost as if Spock or Groovy wants to treat the mocked method as a varargs method of Bytes and is unpacking the byte[] argument. The only reported issue I've been able to find that sounds somewhat like my problem is GROOVY-4843, which was filed against the built-in Groovy mocking framework and has had no resolution.

Is there a way to get test (4) to behave as expected? That is, to be able to use a typed array argument in the closure of one parameter? Or am I stuck using form (3) and having to extract the actual method argument from the untyped closure argument?

Upvotes: 0

Views: 4459

Answers (1)

Alex K.
Alex K.

Reputation: 774

The short answer: there is no normal way to do it, because it's a bug. Only hacks&tricks.

Here is the explanation: your closure is invoked in CodeResponseGenerator::invokeClosure.

  private Object invokeClosure(IMockInvocation invocation) {
    Class<?>[] paramTypes = code.getParameterTypes();
    if (paramTypes.length == 1 && paramTypes[0] == IMockInvocation.class) {
      return GroovyRuntimeUtil.invokeClosure(code, invocation);
    }

    code.setDelegate(invocation);
    code.setResolveStrategy(Closure.DELEGATE_FIRST);
    return GroovyRuntimeUtil.invokeClosure(code, invocation.getArguments());
  }

invocation.getArguments return a list of arguments.

public static <T> T invokeClosure(Closure<T> closure, Object... args)

invokeClosure expects varargs, so that when it gets the list of arguments it wraps the list with an array. Therefore, spock passes Object[] { ArrayList [ byte[] ] } to the closure. Meanwhile, the closure knows that it accepts varargs (as you declare byte[]), so it expects that Object[ {byte[]} ] is being passed. Here we get the exception. I believe it's a bug and spock doesn't have to wrap all parameter with an array in JavaMockInterceptor::intercept.

PS: One more funny bug

This one works fine

def test() {
    given:
    Consumer<Set<Integer>> consumer = Mock()
    when:
    consumer.accept([1,2,3] as Set)
    then:
    consumer.accept(_) >> { Set<Integer> integer ->
        assert integer.size() == 3
    }
}

Let's replace Set with List

def test() {
    given:
    Consumer<List<Integer>> consumer = Mock()
    when:
    consumer.accept([1,2,3])
    then:
    consumer.accept(_) >> { List<Integer> integer ->
        assert integer.size() == 3
    }
}

and we get

integer.size() == 3
|       |      |
|       1      false
[[1, 2, 3]]

You can see that instead of List< Integer> we get List< List< Integer>>. To understand why it happens let's get back to the passed arguments. In this case it looks like Object[] { ArrayList [ ArrayList ] }. Closure knows that the input argument is a list, so it takes the first one it can find and uses it.

Upvotes: 1

Related Questions