spock icon indicating copy to clipboard operation
spock copied to clipboard

Add support for mocking final classes/methods using ByteBuddy

Open leonard84 opened this issue 7 years ago • 23 comments

Mockito 2 has experimental support for mocking final classes/methods (https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2#unmockable). Since Spock as of 1.1 also supports ByteBuddy, it should be possible to also support mocking of final classes/methods.

leonard84 avatar May 26 '17 17:05 leonard84

@raphw what are your thoughts about this? How difficult would it be to add this to Spock?

leonard84 avatar May 26 '17 17:05 leonard84

This would absolutely be possible. The way to do it is about as follows:

  1. Attach a Java agent to the current process to get hold of an instance of Instrumentation. This can be done easily by the byte-buddy-agent class.
  2. To mock a class, define any type in the hierarchy of this class to implement the mocking logic inlined into any method of this class. The logic should look something like the following:
class Foo {
  void bar() {
    if (SpockDispatcher.isMock(this)) {
      SpockDispatcher.doMock(this);
    }  else {
      /* original code */
    }
  }
}

This can be done fairly easily by using the Advice component in Byte Buddy. It works similar to the current interception logic, only that the code is copied to any method. A redefinition can be done by registering a ClassFileTransformer to the instrumentation instance and to redefine the mock classes using it. 3. To make the SpockDispatcher above available to any class loader, it needs to be in its own artifact and must be injected into the bootstrap class loader prior to mocking the first class. This can be done easily by the instrumentation instance.

There are some more details to take care of; but those are not to difficult to solve. I can certainly help with the implementation, the Mockito InlineMockMaker should serve as a good blueprint. I am currently a bit busy with Java 9 support in Byte Buddy, but please ping me if you want to implement it.

raphw avatar May 29 '17 05:05 raphw

@raphw @leonard84 Is there any progress on this? I'm trying to use Spock on Kotlin code, and their decision to make objects and data classes always final is painful when testing.

henrik242 avatar Sep 29 '17 14:09 henrik242

@henrik242 no, there are some other issues we are working on with higher priority right now.

leonard84 avatar Sep 29 '17 16:09 leonard84

will just confirm that android+kotlin+spock is a heavy issue with it. probably the same will be with springboot applicaitons that are written in kotlin. any class/method mocked need to be open

clydzik avatar Nov 10 '17 13:11 clydzik

@clydzik yes we are aware of that, if you are using spring you'd need to use the allopen plugin anyway. So in the meantime I'd suggest using it, this way you don't have to make your classes explicitly open.

leonard84 avatar Nov 12 '17 20:11 leonard84

The allopen plugin will make all spring-annotated classes non-final, but for other classes you still have to use the open keyword. Or create a new annotation (e.g. com.example.Open), and annotate your classes with it. And a data class needs the annotation in order to be non-final, since it doesn't support the open keyword.

henrik242 avatar Nov 13 '17 09:11 henrik242

@leonard84 thank You for clarification. My actual scope is android. And i will probably use an open annotation temporary but still using this feature for test purposes can be seen as 'turning of language feature for tests'. That is why i'm pushing this forward.

I would be grateful if You could address for a solution in close future. And thank You for fast response.

clydzik avatar Nov 13 '17 11:11 clydzik

For the benefit of anyone reading this I have a workaround, see also section "Update 2" of my answer on StackOverflow. There is also sample code there. I am quoting the essential part from there:

Add this to your Maven build (if you use Gradle, use something similar):

<dependency>
  <groupId>de.jodamob.kotlin</groupId>
  <artifactId>kotlin-runner-spock</artifactId>
  <version>0.3.1</version>
  <scope>test</scope>
</dependency>

Now you can use the SpotlinTestRunner from project kotlin-testrunner in combination with annotations like

  • @OpenedClasses([Foo, Bar, Zot])
  • @OpenedPackages(["de.scrum_master.stackoverflow", "my.other.package"])

and just use normal Spock mocks for final Java or Kotlin classes.

I would still love to see this in Spock, just like mocking static methods in Java classes. This way I would no longer need additional tools like the one above or PowerMock. BTW, I do know that using things like mocking static methods is a smell. But sometimes we have to use nasty 3rd party libs and test integration with them somehow...

kriegaex avatar Jan 24 '18 02:01 kriegaex

@kriegaex thanks for the info

leonard84 avatar Jan 24 '18 15:01 leonard84

+1 on this issue. I LOVE Spock for many, many reasons, and I would like to not have to use another library like mockk https://github.com/oleksiyp/mockk (which by the way mocks Kotlin's Object i.e. Singleton etc)

RaviH avatar Feb 27 '18 19:02 RaviH

+1 on this one. I just ran into this problem while trying to mock a final method (unfortunately from a third-party lib I cannot modify), and it'd be nice to have it. Otherwise I'll just have to fall back to Mockito (which I didn't want since it becomes increasingly verbose), as I don't want to have a lot of complimentary plugins and dependencies. Is there any ETA about supporting this? To understand timings and considering that I'm working with Kotlin, to check if Spock will be a viable option to write the tests. Thanks!

sescotti avatar Jun 11 '18 09:06 sescotti

+1 from me, testing Kotlin with Spock is painful without this

@sescotti consider creating a non-final decorator for the final third party class.

Jezorko avatar Nov 09 '18 12:11 Jezorko

@leonard84 Any word on this?

RaviH avatar Nov 09 '18 17:11 RaviH

Sorry, I won't be working on this in the near future, but I someone wants to give it a try they should be able to find the necessary information in this ticket.

From a quick look the SpotlinTestRunner mentioned above looks promising, although I didn't test it. It may even be the better approach since, the mockito approach works by basically turning every object into a potential mock. This has a performance impact on all executed code, since it always has to check whether the current instance should be intercepted.

leonard84 avatar Nov 29 '18 12:11 leonard84

FYI, As the Sputnik runner was removed in commit fa8bd57cbb2decd70647a5b5bc095ba3fdc88ee9 for Spock 2.x, the SpotlinTestRunner approach no longer works there. Either that test runner needs to be converted into a Java agent or we have to look at the ByteBuddy approach again.

kriegaex avatar Apr 05 '20 13:04 kriegaex

or we have to look at the ByteBuddy approach again.

That would be awesome :)

henrik242 avatar Apr 05 '20 18:04 henrik242

Has there been any update on this? I REALLY love Spock's mocking syntax and ease-of-use, but it is absolutely broken for Kotlin classes without the use of "open". Unfortunately my classes need to remain final for security purposes, I imagine that's a pretty common use-case for most developers using kotlin

meotch avatar May 12 '20 22:05 meotch

@Vampire and I discussed some approaches but nothing has been implemented yet.

leonard84 avatar May 14 '20 22:05 leonard84

Given the fact that the Spotlin runner requires Sputnik and the latter is no longer contained in Spock 2.0-M2, is anyone interested in a solution (or workaround until it has been implemented in Spock, whatever you want to call it) I lately implemented as a by-product of another tool I am developing. Short feature overview:

  • removes final from all target classes and methods
  • small library (~125 KB including shaded-in ASM files used internally)
  • can run as a Java agent (you just put it on the Java command line via -javaagent:/path/to/library.jar); also easily done in Maven Surefire/Failsafe
  • can also be dynamically attached when starting your test if your JVM can use the Java attach API (e.g. tools.jar for Java 8, feature activated via system property for Java 9+); you can do it manually or just use ByteBuddyAgent for it (don't worry if you don't know what that is for now)
  • can even be used from Maven via a plugin in order to instrument JRE classes like String, StringJoiner, URL which normally are already loaded before a Java agent starts. It will "unfinal" them, zip them up in a separate JAR which then you can prepend to the Java boot classpath (Java 8) or use via "patch module" (Java 9+), effectively replacing the unmockable originals which then you can easily mock with normal Spock mocks, just like any other class.

Of course it also works with normal Java application or library classes and also with non-open Kotlin classes. It works in Java/JUnit, Groovy/Spock/Geb because it is a universal solution. You do not need any other mocking tools such as Mockito, EasyMock, PowerMock or JMockit which all I find difficult to use for that simple mattter.

If there is significant public interest, I can polish this stuff a bit and point you to the corresponding GitHub repository. If the interest should be unexpectedly overwhelming, I can even publish a version on Maven Central for easier availability, so nobody would have to build the library by themselves.

If @leonard84 thinks it is a good idea, I can advertise this on the Gitter channel and on the mailing list too. But I don't want to bother anyone. It works nicely for me, I scratched my own itch because nobody else did.


BTW, as I said the "remove final agent" library is part of another private project which is based on ByteBuddy and already working, but still not very polished, rather a proof of concept. What you can do with it is intercept any method or constructor, even static type initialisers (static blocks in classes) and do before/after actions there, e.g.

  • suppress static initialiser execution,
  • change constructor parameters before execution,
  • throw an exception in order to stop a constructor from being executed (but you cannot suppress it silently right now, for that we have Objenesis),
  • change constructor parameters before execution,
  • dynamically decide if methods should be executed or not,
  • do something after/instead of method execution, including catching exceptions, throwing (other or same) exceptions, manipulating the method's return value).
  • You can do all this globally (similar to a global Groovy Spy, but for Java objects too) or per instance (similar to a normal Groovy Spy).
  • It works for static methods too, i.e. you can stub them.
  • It even works for static or instance methods of already loaded classes because it modifies only the byte code inside methods (which is allowed in retransformations).
  • It even works for already loaded JRE classes like String because the agent is capable of injecting itself into the bootstrap classloader. This was difficult to implement, but now it works nicely.

I think these two tools can help making mock testing easier in difficult cases. I wrote them for myself, but also as a template and proof of concept for things I would like to see directly in Spock so as to have "one tool to rule them all" and get rid of PowerMock and JMockit for good.

kriegaex avatar May 16 '20 12:05 kriegaex

@kriegaex sure you can publish your solution. If it helps those who desperately need such a solution then I don't see a reason why it shouldn't be shared.

leonard84 avatar Jul 01 '20 15:07 leonard84

Just stumbled upon https://github.com/joke/spock-mockable which fixes this problem:

Add the @Mockable annotation to your spock specification. Make Person mockable

@Mockable(Person)
class PersonSpec extends Specification {
}

More examples can be found in examples.

henrik242 avatar Nov 03 '21 12:11 henrik242

Forgot to mention it here, but we have it already listed in the Wiki https://github.com/spockframework/spock/wiki/Third-Party-Extensions

leonard84 avatar Nov 03 '21 16:11 leonard84

How about we would integrate Mockito as optional dependency as we do it with byte-buddy or cglib. And use the API of Mockito org.mockito.plugins.MockMaker with a custom org.mockito.invocation.MockHandler to implement the Spock logic.

This would be much easier for the issue, because mocking final classes and/or final methods have many strange implications. You can't change the final modifier of a class or method, if the class was already loaded, see jvmti-Spec Error JVMTI_ERROR_UNSUPPORTED_REDEFINITION_CLASS_MODIFIERS_CHANGED: A new class version has different modifiers, which would lead to failing tests e.g. in concurrent execution or different class loading orders, except we do Classloader trickery.

Mockito works around that, by not removing the final modifier, but retransforming the methods in the classes to mock, with the org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker. But we would need a combination of that, as Mockito does it, because if you want to mock a final method, but implement an additional interface you need to combine both features InlineByteBuddyMockMaker and SubclassByteBuddyMockMaker. The SubclassByteBuddyMockMaker of Mockito, does the similar thing as Spocks ByteBuddyMockFactory.

The code to handle all these cases is very elaborate in Mockito, it is well tested and JVM specific, so it can be a maintenance problem, if we would reimplement all this in Spock itself. To get a feel for the complexity, just have a look at the class org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker and browse a bit :).

So as I said, I would try to use Mockito for these cases in Spock as an optional dependency. Could you guys tell me, if one of you object to that? Otherwise, I would give that idea a shot.

AndreasTu avatar Aug 02 '23 16:08 AndreasTu

Adding a mockito mock factory would relate to #1220, which would create the infrastructure to add additional mock factories.

We would also need to think about a better way to define which mock factory is used, currently we do a feature detection and have a system property to turn of byte buddy, but that doesn't scale well and is an all-or-nothing kind of switch.

As for the additional interfaces, I read that Mockito 5 is using inline as new default and only offers subclass as an optional dependency. IMHO it would be acceptable for a mock factory to not support all features, e.g., you can't use both global: true and additionalInterfaces a groovy mock.

The inline mocking would actually allow global: true for java mocks as well, right?

leonard84 avatar Aug 04 '23 14:08 leonard84

What does the global: true currently do?

AndreasTu avatar Aug 04 '23 15:08 AndreasTu

@AndreasTu: Actually, a few years ago, I already solved all those problems and several more in Sarek, see https://github.com/spockframework/spock/issues/735#issuecomment-629638278. I made sure that Sarek uses the same optional dependencies as Spock - Byte Buddy and ASM - in order to avoid adding Mockito or anything else on top of it. Besides, Sarek is more powerful than Mockito, even though Mockito has caught up with inline mocks.

Back then, I intended to donate the source code to Spock and simply keep Sarek around as an incubator or for users who want to use the same features in other testing tools such as JUnit 4/5 or TestNG. The donation never happened, because it was rejected. Leonard did not want to maintain the additional code, even though I would have helped to do so, of course. Then we had the idea of keeping Sarek separate, but create a Spock mock factory for it. In order to make that possible, things like #1220 were put into the pipeline. Leonard suggested me to just start developing the mock factory and tell him where I needed more features. I knew too little about Spock's internal infrastructure to dare to tackle that on my own, given my limited time budget outside my regular job, but we never got a pair programming approach off the ground, even though it would have saved lots of time and iterations. Therefore, I simply shelved Sarek at the time, because I really prefer collaborative development.

kriegaex avatar Aug 06 '23 05:08 kriegaex

@leonard84 Should this be assigned to the 2.3 milestone?

marcphilipp avatar Oct 23 '23 11:10 marcphilipp

2.4 actually. 2.3 is already released. The milestone just was not closed and new created.

Vampire avatar Oct 23 '23 12:10 Vampire