Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mockito with Groovy fails to create stubs for partial mock for abstract class derivative #1108

Open
vadim-ag opened this issue Jun 2, 2017 · 13 comments

Comments

@vadim-ag
Copy link

vadim-ag commented Jun 2, 2017

I am using Mockito 2.8.9 with Groovy 2.4.1 and JUnit 4.12 (openjdk version "1.8.0_131")

The setup:

  • I have an instance of the Failure class that is derived from abstract AbstractBase parent.

  • I am stubbing partial mock (spy) using doXxx methods.

  • => failure

  • For contrast, the above steps are repeated with Success which is derived from regular Base parent.

  • => success

The tests:

import org.junit.Test
import static org.mockito.Mockito.*

abstract class AbstractBase { }

class Failure extends AbstractBase {
    def method() { }
}

class Base { }

class Success extends Base {
    def method() { }
}

class FooTest {
    @Test
    void failed() {
        def sut = spy(new Failure())
        doReturn(1).when(sut).method()
    }

    @Test
    void succeed() {
        def sut = spy(new Success())
        doReturn(1).when(sut).method()
    }
}

As test names suggest, succeed() works as expected, while failed() generates an error with following message and stack trace (integer is incidental, I verified with several types):


org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Integer cannot be returned by getMetaClass()
getMetaClass() should return MetaClass
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
   Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies - 
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.


	at org.codehaus.groovy.runtime.callsite.CallSiteArray.createPogoSite(CallSiteArray.java:147)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:164)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
	at FooTest.failed(FooTest.groovy:24)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Thank you!

P.S. I ran it also with Groovy 2.4.10, and with Mockito 2.7.22 -- with the same result

@mockitoguy
Copy link
Member

Thank you for reporting! We don't normally use Mockito with Groovy code because the best unit testing framework for Groovy is Spock framework which already contains great support for mocking.

By now, did you find a way to resolve the problem? If not, do you want to help identifying the root cause? Can you try with Mockito latest 1.x and see if it works? Mockito 1.x used CGLIB for mocking and I'm wondering if the behavior was the same for this scenario. Also, you can try debugging Mockito code and see in what way the internal behavior is different for the succeeded() and failed() methods.

@vadim-ag
Copy link
Author

vadim-ag commented Jun 6, 2017

First and foremost, thank you very much.

We don't normally use Mockito with Groovy...

Legacy codebase dictates options. :-(

By now, did you find a way to resolve the problem?

I was able to convert class to non-abstract, but it was sub-optional.

If not, do you want to help identifying the root cause?

Absolutely!

1.x

I had troubles running 1.9.5 (but probably for unrelated reasons). Let me give it a try.

Also, you can try debugging Mockito code and see in what way the internal behavior is different for the succeeded() and failed() methods.

It is a bit difficult. I am new to Groovy / Java / Mockito world, and not quite sure how to run local build dependency. Decompiled code is not fun to run at all. I will do my best.

@mockitoguy
Copy link
Member

Mockito ships source code to Maven Central. So long you configure your project using model build tools like Gradle or at least Maven, the source code should be picked up automatically by the IDE. Just run test with debugging, and step to the code to debug issues like this one. If you don't feel bold enough to dive into debugging, consider creating a tiny project / repository where the problem can be reproduced. Then perhaps somebody from the core team or some other contributor can pick up the work!

I'll mark this ticket as "please-contribute". Thank you for supplying the Test sample - it is very useful for reproducing issues.

@sluck2013
Copy link

@szczepiq I debugged through the code, looks like byte buddy doesn't work well with Groovy, do you have any suggestions on fixing this?

Continuing with the example in this post, in the "failed" test, the method parameter returned by @Origin annotation is always AbastractBase.getMetaClass() (which returns a groovy.lang.MetaClass object) instead of the method we expect, and hence fails the return type validation. But in the "succeed" test, the method reflects the one we expect, and return type is Object

@dee-kryvenko
Copy link

I've tried with 1.10.19 and 1.9.5 and still getting the same exception.

@mockitoguy
Copy link
Member

I don't have an answer right off the bat. The issue needs to be debugged. I'll ping @raphw (founder of Bytebuddy and core Mockito developer) in case he can / have time to help out.

Side question: why don't you use Spock framework for testing and mocking Groovy code?

@dee-kryvenko
Copy link

Me personally - I think I ended up using Spock. I was evaluating frameworks for a new Groovy project (which is a Jenkins shared library for it's 2.0 Pipelines) and so far Spock looks like a best fit. But from the top of my head the answer could be - this is not a Groovy project but rather a Java project with some Groovy. Even now I still not sure will Spock play nice in terms of mocking when it will come to testing code that is extensively using Java libs such as apache-commons which is Java and built all of a static methods. Hope it will. Otherwise I will have to look at Mockito together with PowerMock once again.

@kshep92
Copy link

kshep92 commented Jun 25, 2018

Might be a little late to contribute, but I was running into a similar issue with the following code:

abstract class DAO {}

class PersonDAO extends DAO {
  Person findById(Long id)
}

class PersonDAOTest {
  @Test
  void fails() {
    def dao = Mockito.mock(PersonDAO)
    Mockito.given(dao.findById(Mockito.anyLong())).willReturn(new Person()) // Fails with a MatchersException
    dao.findById(1L)
   assertNotNull(person)
  }
}

What worked for me was converting the abstract class (DAO) to Java 🤷‍♂️

@agabrys
Copy link

agabrys commented Oct 19, 2020

I found a working workaround. It requires some code changes, but only in tests. You can add the groovy.transform.CompileStatic annotation to the test class. I checked if for the above example.

Sources

package issue1108

abstract class DAO {}
package issue1108

class Person {}
package issue1108

class PersonDAO extends DAO {

	Person findById(Long id) {
		return new Person()
	}
}

Broken Test Class

package issue1108

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when

import org.junit.jupiter.api.Test
import org.mockito.Mockito

class PersonDAOTest extends DAO {

	@Test
	void fails() {
		def dao = mock(PersonDAO)
		when(dao.findById(Mockito.anyLong())).thenReturn(new Person())
		def person = dao.findById(1L)
		assertNotNull(person)
	}
}

failure

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
0 matchers expected, 1 recorded:
-> at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(anyObject(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(anyObject(), eq("String by matcher"));

For more info see javadoc for Matchers class.

	at org.codehaus.groovy.runtime.callsite.CallSiteArray.createPogoSite(CallSiteArray.java:146)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:163)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:128)
	at issue1108.PersonDAOTest.fails(PersonDAOTest.groovy:15)
	[...]

Successful Test Class

package issue1108

import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when

import groovy.transform.CompileStatic
import org.junit.jupiter.api.Test
import org.mockito.Mockito

@CompileStatic
class PersonDAOTest extends DAO {

	@Test
	void fails() {
		def dao = mock(PersonDAO)
		when(dao.findById(Mockito.anyLong())).thenReturn(new Person())
		def person = dao.findById(1L)
		assertNotNull(person)
	}
}

success

@solvingj
Copy link

We have a bunch of dynamic class loading happening, can't use @CompileStatic at the moment. Any possibility of a different workaround?

@solvingj
Copy link

I added the following comment to a related ticket with a reproducible example, hopefully it is something that can be resolved:
#2226 (comment)

@eric-milles
Copy link

Groovy 2.5.3 and newer writes @groovy.transform.Internal on methods like getMetaClass(). Mockito looks for this and does not mock/stub those methods. Do your examples run properly on Groovy 2.5.22 (latest 2.5 rel) and better?

@solvingj
Copy link

We use Groovy for Jenkins shared library's. And in Jenkins, the Groovy version is still locked to 2.4.21.

https://issues.jenkins.io/plugins/servlet/mobile#issue/JENKINS-51823

Whether or not to invest time into this issue, given that it only affects users of Groovy, and only very old versions of Groovy at that, is a valid question. I will point out that vast numbers of people are using Groovy with Jenkins, and the subset of those which are also trying to use mockito is probably also significant, but that still might not be enough to warrant further investment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants