Using Groovy MetaClass to overwrite Methods
Asked Answered
T

3

16

I have a POJO that uses a service to do something:

public class PlainOldJavaObject {

    private IService service;

    public String publicMethod(String x) {
        return doCallService(x);
    }

    public String doCallService(String x) {
        if(service == null) {
            throw new RuntimeException("Service must not be null");
        }
        return service.callX(x);
    }

    public interface IService {
        String callX(Object o);
    }
}

And I have a Groovy test case:

class GTest extends GroovyTestCase {

    def testInjectedMockIFace() {
        def pojo = new PlainOldJavaObject( service: { callX: "very groovy" } as IService )
        assert "very groovy" == pojo.publicMethod("arg")
    }

    def testMetaClass() {
        def pojo = new PlainOldJavaObject()
        pojo.metaClass.doCallService = { String s ->
            "no service"
        }
        assert "no service" == pojo.publicMethod("arg")
    }
}

The first test method, testInjectedMockIFace works as expected: The POJO is created with a dynamic implementation of IService. When callX is invoked, it simply returns "very groovy". This way, the service is mocked out.

However I don't understand why the second method, testMetaClass does not work as expected but instead throws a NullPointerException when trying to invoke callX on the service object. I thought I had overwritten the doCallService method with this line:

pojo.metaClass.doCallService = { String s ->

What am I doing wrong?

Thanks!

Turgeon answered 18/12, 2009 at 11:55 Comment(0)
M
20

Your syntax is a tiny bit off. The problem is that pojo is a Java Object and does not have a metaClass. To intercept calls to PlainOldJavaObject's doCallService using ExpandoMetaClass:

Just replace:

    pojo.metaClass.doCallService = { String s ->
        "no service"
    }

With:

    PlainOldJavaObject.metaClass.doCallService = { String s ->
        "no service"
    }
Montagnard answered 8/3, 2010 at 20:12 Comment(2)
One thing to keep in mind here is when you manipulate the Class's metaClass, every instance from that point forward will be manipulated. This can have a big impact on other tests that run in the same session. When you manipulate an instance of a class, only that instance is affected.Hypotonic
To achieve complete test isolation, so that your manipulation on the metaClass won't impact other tests you could do the following. Remember the old method (eg., def oldMethod = pojo.&doCallService) override it (pojo.metaClass.doCallService = { String s -> "no service" }) at the beginning of your test and return the old method at the end of the test (pojo.metaClass.doCallService = oldMethod). NOTE: this will only work if the method doCallService is NOT overloaded (multiple doCallService methods with different arguments) since 'def oldMethod = pojo.&doCallService' can't know which to take.Wallinga
H
18

If your POJO really is a Java class, and not a Groovy class, then that is your problem. Java classes don't invoke methods via the metaClass. e.g., in Groovy:

pojo.publicMethod('arg')

is equivalent to this Java:

pojo.getMetaClass().invokeMethod('publicMethod','arg');

invokeMethod sends the call through the metaClass. But this method:

public String publicMethod(String x) {
    return doCallService(x);
}

is a Java method. It doesn't use invokeMethod to call doCallService. To get your code to work, PlainOldJavaObject needs to be a Groovy class so that all calls will go through the metaClass. Normal Java code doesn't use metaClasses.

In short: even Groovy can't override Java method calls, it can only override calls from Groovy or that otherwise dispatch through invokeMethod.

Hercules answered 28/12, 2009 at 20:59 Comment(2)
How do you properly distinguish between Groovy code and Java code? How would you make PlainOldGroovyObject instead of PlainOldJavaObject?Brittaneybrittani
If it's in a .groovy file, it's a Groovy class.Hercules
D
1

What you have looks fine. I ran a slightly modified version on it on the groovy console webapp and it ran without issue. See for yourself using this code at http://groovyconsole.appspot.com/.

public interface IService {
    String callX(Object o);
}

public class PlainOldJavaObject {

    private IService service;

    public String publicMethod(String x) {
        return doCallService(x);
    }

    public String doCallService(String x) {
        if(service == null) {
            throw new RuntimeException("Service must not be null");
        }
        return service.callX(x);
    }
}

def pojo = new PlainOldJavaObject()
pojo.metaClass.doCallService = { String s ->
    "no service"
}
println pojo.publicMethod("arg")

What version of Groovy are you using. It could very well be a bug in Groovy in the metaclass implementation. The groovy language moves pretty quickly and the metaclass implementation changes from version to version.

Edit - Feedback from Comment:

The version of the groovy console webapp is 1.7-rc-1. So it looks like that version may work as you want it to. They are currently in RC2 so I expect it would be released soon. Not sure if what you are seeing is a bug or just a difference in how it works in the 1.6.x version.

Dolomite answered 18/12, 2009 at 12:21 Comment(2)
Hi Chris, I run Groovy Version: 1.6.5 JVM: 1.6.0_13Turgeon
The version has nothing to do with it. The problem is that doCallService(x) is Java code, not Groovy code, so it isn't metaClass aware.Hercules

© 2022 - 2024 — McMap. All rights reserved.