Reputation: 1141
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 Byte
s 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
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