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!

Esko
  • 29,022
  • 11
  • 55
  • 82
raoulsson
  • 14,978
  • 11
  • 44
  • 68

3 Answers3

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"
    }
l15a
  • 2,547
  • 5
  • 29
  • 41
  • 5
    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. – A.J. Brown May 06 '13 at 15:58
  • 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. – Mate Šimović Mar 22 '19 at 10:23
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.

Maicon Mauricio
  • 2,052
  • 1
  • 13
  • 29
noah
  • 21,289
  • 17
  • 64
  • 88
  • How do you properly distinguish between Groovy code and Java code? How would you make PlainOldGroovyObject instead of PlainOldJavaObject? – Tomato Dec 13 '12 at 10:36
  • 5
    If it's in a .groovy file, it's a Groovy class. – noah Dec 13 '12 at 15:37
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.

Chris Dail
  • 25,715
  • 9
  • 65
  • 74
  • Hi Chris, I run Groovy Version: 1.6.5 JVM: 1.6.0_13 – raoulsson Dec 18 '09 at 12:45
  • 1
    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. – noah Dec 28 '09 at 21:01