Groovy casting collection unasked for it
Asked Answered
C

4

9

I have some code written in Java that uses Generics. This is a simple version:

// In Java
public interface Testable {
    void test();
}

public class TestableImpl implements Testable {
    @Override
    public void test(){
        System.out.println("hello");
    }
}

public class Test {
    public <T extends Testable> void runTest(Collection<T> ts){
        System.out.println("Collection<T>");
        for(T t: ts)
            t.test();
    }

    public void runTest(Object o){
        System.out.println("Object");
        System.out.println(o);
    }
}


// in Groovy - this is how I have to use the code
Test test = new Test()
test.runTest([new TestableImpl(), new TestableImpl()]) 
test.runTest([1,2,3]) //exception here

I am suprised that the second method call is dispatched to the wrong method (wrong in my Javish understanding). Instead calling the Object overload, the Collection gets called.

I am using Groovy 2.1.9, Windows 7.

And the exception is:

Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: 
   Cannot cast object '1' with class 'java.lang.Integer' to class 'Testable'
org.codehaus.groovy.runtime.typehandling.GroovyCastException:
   Cannot cast object '1' with class 'java.lang.Integer' to class 'Testable'

Why? How to solve this?

How to make Groovy call the same method as Java would?


edit: to further explain the case, I'd like to write a Spock test for it (just imagine the method returns something, say a String..):

def "good dispatch"(in,out) {
    expect:
    test.runTest(in) == out

    where:
    in                   | out
    new Object()         | "a value for Object"
    new Integer(123)     | "a value for Object"
    [1,2,3]              | "a value for Object"
    [new TestableImpl()] | "a value for Testable Collection"

}
Caldron answered 9/1, 2014 at 12:28 Comment(1)
Maybe related: #10932788Machute
B
8

Others have suggested possible ways to solve your problem but here is WHY it happens.

Groovy - being a dynamic language - uses the runtime type information to invoke the correct method. Java, on the other hand, determines which method will be used based on the static type.

A simple example that demonstrates the differences between JAVA and GROOVY:

void foo(Collection coll) {
    System.out.println("coll")   
}

void foo(Object obj) {
    System.out.println("obj")   
}

In GROOVY:

Object x = [1,2,3] //dynamic type at invocation time will be ArrayList
foo(x)
//OUT: coll

In JAVA:

Object x = Arrays.asList(1,2,3);
foo(x);
//OUT: obj
Collection x = Arrays.asList(1,2,3);
foo(x);
//OUT: coll

Now in your example (it does not really have anything to do with the use of generics):

test.runTest([new TestableImpl(), ...]) //ArrayList --> collection method will be used
test.runTest([1,2,3]) //also ArrayList --> best match is the collection variant
Bilestone answered 3/2, 2014 at 14:19 Comment(3)
You have explained the problem, but did not provide an answer on how to avoid it.Escheat
true, and if you read my first sentence thats exactly what I wrote. one part of the question was WHY?. I answered that. One could use @CompileStatic or type casts as mentioned in the other answers - both approaches with their own problems. Simply put I dont think there is an easy way to accomplish this (at least I dont know one). It is also kind of fighting the system since Groovy is a dynamic language after all :) If you want to remove all these features then you might as well just stick to java (allthough I understand what OP wants to achieve, its really neat to write tests in groovy).Bilestone
@tfeichtinger Thanks. I wanted, indeed, to test some code in Groovy. I wanted to have a "data provider" - multiple intputs to a single test method (be it in TestNG or Spock).. and the whole point was to check method overloads etc. It therefore seems, that it's dangerous to test Java in Groovy (because the code is working differently) and sometimes (like here) it's impossible (because the difference is the point of test) I'm still waiting for some Guru to speak up :-) Anyway, thank you for your input.Caldron
A
3

If multiple dispatch is not what you want, can you cast the argument in the test script?

test.runTest( (Object) [1,2,3] )
Antiquate answered 9/1, 2014 at 13:52 Comment(1)
+1 This seems like the best solution since only the call site needs to be tweaked. Note that test.runTest([1,2,3] as Object) also works, based on your linked article.Shiau
C
1

This happens because Java strips the generic information from the code at compile time.

When Groovy tried to select the correct method at runtime, it gets an ArrayList as parameter for the second call (note: No generic information anymore) which matches runTest(Collection tx) better than runTest(Object o).

There are two ways to solve this:

  • Create two methods with different names
  • Delete runTest(Collection). Instead use instanceof in runTest(Object) to determine whether the argument is a collection of the correct type and delegate to a new internal method runTestsInCollection().
Catalog answered 9/1, 2014 at 12:47 Comment(4)
This is bad news, I wanted to use Groovy for test code of some Java utilities that I wrote... and I don't want to change the API just because I test in Groovy.. I am using Spock frameworkCaldron
Create a helper class GroovyTest which extends or delegates to Test after doing the checks. Alternatively, with Groovy 2, you can try @CompileStatic.Catalog
Do you know Spock framework? It has some crazy features that simply wont work when compiled statically, they actually wont compile at all :-(Caldron
I know a little about Spock. If @CompileStatic, then use the helper class which translates between Groovy and Java.Catalog
D
1

Lets start from the solution:

import groovy.transform.CompileStatic
import spock.lang.Specification
import spock.lang.Subject

class TestSpec extends Specification {

    @Subject
    Test test = new Test()

    def 'Invokes proper method from JAVA class'() {
        given:
        List input = [1,2,3]

        when:
        invokeTestedMethodJavaWay(test, input)

        then:
        noExceptionThrown()
    }

    @CompileStatic
    void invokeTestedMethodJavaWay(Test test, Object input) {
        test.runTest(input)
    }
}

First of all, you cannot override methods by generic type even in JAVA. For example if you try adding another method with same contract but overloaded with different generic type, let say public <P extends Printable> void runTest(Collection<P> ps) you will run into disambiguation problem as both methods will have same erasure.

What's more important in your question, has been already stated in other answers here. Your expectations didn't meet the behaviour as we are getting into compile vs runtime types evaluation between respectively JAVA and Groovy. This can be very useful if one is aware of this. For example when handling exceptions. Consider following example.

JAVA:

public void someMethod() {
    try {
        // some code possibly throwing different exceptions
    } catch (SQLException e) {
        // SQL handle
    } catch (IllegalStateException e) {
        // illegal state handle
    } catch (RuntimeException e) {
        // runtime handle
    } catch (Exception e) {
        // common handle
    }
}

Groovy:

void someMethod() {
    try {
        // some code possibly throwing different exceptions
    } catch (Exception e) {
        handle(e)
    }
}

void handle(Exception e) { /* common handle */ }

void handle(IllegalStateException e) { /* illegal state handle */ }

void handle(RuntimeException e) { /* runtime handle */ }

void handle(SQLException e) { /* SQL handle */ }

I find Groovy way much cleaner than nasty try-catch multi block, especially that you can implement all handle methods in separate object and delegate handling. So it's not a bug, it's a feature :)

Getting back to the solution. You cannot annotate whole Spock's test class with @CompileStatic as you already know. However you can do this with a single method (or separate helper class). This will bring back expected java-like behaviour (compile time type evaluation) for any call from within annotated method.

Hope this helped, cheers!

PS. @Subject annotation is only used for the sake of readability. It points which object is under test (is subject of the specification).

EDIT: After some discussion with the author of the question, not so clean but working solution:

import groovy.transform.CompileStatic import spock.lang.Specification import spock.lang.Subject

class TestSpec extends Specification {

    @Subject Test test = new Test()
    TestInvoker invoker = new TestInvoker(test)

    def 'Invokes proper method from JAVA class'() {
        when:
        invoker.invokeTestedMethod(input)

        then:
        noExceptionThrown()

        where:
        input << [
                [1, 2, 3, 4, 5],
                [new TestableImpl(), new TestableImpl()]
        ]
    }
}

@CompileStatic
class TestInvoker {

    Test target

    TestInvoker(Test target) {
        this.target = target
    }

    void invokeTestedMethod(Object input) {
        target.runTest(input)
    }

    void invokeTestedMethod(Collection input) {
        if (input.first() instanceof Testable) {
            target.runTest(input)
        } else {
            this.invokeTestedMethod((Object) input)
        }
    }
}

If you would need to check by more than one generic type of collection, please notice that instanceof can be used in switch case statements in Groovy.

Donica answered 5/2, 2014 at 12:4 Comment(5)
This is nice. Can you call a @CompileStatic method from a data table in spock? (docs.spockframework.org/en/latest/…) ? This is what I'd be satisfied with!Caldron
Do you mean the table in the where block? Yes, you can. I thought that such method needs to with static modifier then, but it seems that this is not necessary. @CompileStatic over helper method does not influence whether it can or can't be called from where block's data table.Donica
Hey, but your invokeTestedMethodJavaWay will always call the Object-overload version, because the static type of argument is Object.. I'd love to accept this answer but you have to make my case work - in the original post I run 2 invocations for 2 overloads, and in reality I'd want to test ca. 5 overloads with ca. 25 data inputs.Caldron
Could you please provide an example how would look the invocation you want in plain JAVA? If you would do JAVA equivalent call test.runTest(new ArrayList<Integer>() {{ add(1); add(2); add(3); }}) you are going to have ClassCastException as well. It rather seems that you are trying to overload by generic type of method parameter, which is not possible in JAVA. Sorry for this confusing ArrayList instantiation but I have found no other way of doing this inline.Donica
Well, I see what would you like to achieve. Groovy dynamic types evaluation would be really helpful in your case except the fact that you are trying to overload by method parameter generic type. Which is not possible either in JAVA or Groovy. That's why I would be happy to see how this could be solved in JAVA as you stated that you don't like to alter your clesses API only because they are tested with groovy. Anyway, I added not-so-straight-forward but working solution to my answer. Maybe it will do the job.Donica

© 2022 - 2024 — McMap. All rights reserved.