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
Added support for mock class creation using Byte Buddy #626
Conversation
@@ -3,6 +3,6 @@ | |||
<component name="VcsDirectoryMappings"> | |||
<mapping directory="" vcs="Git" /> | |||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, duplicated entries look strange.
Thanks Rafael. I like the way Byte Buddy works in Mockito and I would be happy seeing it in Spock. +1 for merging it before 1.1.0-final (as it could potentially change the default behavior if Byte Buddy is already on a classpath). |
Glad to hear. There are some things I would like to mention before merging this:
What is your view on that? |
On a side note: IntelliJ files should not be version controlled. Those files are considered internal and my installation just picked up the files and changed them without me even noticing. This happens every time I open the project. |
Sounds to be useful and quite safe to me.
I think that currently Spock doesn't support additional interfaces in mocks at all.
I cannot recall any situation when I hit that issue, however, I can image that it would be good to support that. I think that 1.1 already has introduced some incompatibilities and IMHO that one would be acceptable.
There are already some issues with mocking generic types in Spock. I would need to dig into to say how does it look. However, there is one issue that came to my mind. Currently all internal tests in Spock have been switched to ByteBuddy (as it is on a classpath) and we lost regression testing against cglib (in Mockito cglib support was dropped, so it was not a problem). I don't know how much feasible is to already have Byte Buddy on a classpath (as a 3rd party dependency?) in the real project and still want to use cglib in Spock, but in a case of regression people may want to be able to switch to cglib for particular mocks (like additional configuration parameter in IMockConfiguration). I don't have a good idea how to provide testing for both ByteBuddy and cglib without using some global configuration parameter (like a system property) set in an additional Gradle test task (for cglib). However, maybe we don't need it or maybe we may even want to treat cglib support as the 2nd class citizen (aka "deprecated mode")? |
Can we get an example of the bridge method issue? I don't quite follow what we're losing or gaining there. It would be a very good idea to have some kind of testing in place for cglib unless we are dropping support for it entirely. |
The problem with bridge methods is that they are much more complex than they might appear. For this reason, Byte Buddy does not expose bridge methods or requires them to be handled manually in any context. For example, with cglib, you simply exclude any method with the /* package-private*/ class Foo {
public String foo() { return "foo"; }
}
public class Bar extends Foo { } The Java compiler will add a visibility-bridge for public class Bar extends Foo {
public /* synthetic bridge */ String foo() { return super.foo(); }
} By excluding bridge methods from interception, you do no longer mock the Also, cglib does not implement reified methods for a mocked type what can cause problems or change in behavior with some libraries and tools. So to speak, if you are mocking some: public class Foo<T> {
public void foo(T val) { }
}
public class Bar extends Foo<String> { } Byte Buddy will add the correct bridge when instrumenting public class Bar$SpockMock extends Bar {
public void foo(String val) { }
public /* synthetic bridge */ void foo(Object val) { foo((String) val); }
} Note that some frameworks will expect to being able to look up the actual method via a call similar to As for testing cglib: Maybe Spock can supply a system property that decides if Byte Buddy is considered. This would allow both to (a) retain old testing behavior even if Byte Buddy is already on the class path (b) to run a build with mocks created by cglib. As an alternative, in Mockito, this is solved by using plugins where a build can register a custom byte code provider. |
Class<?> enhancedType = CACHE.find(classLoader, type, additionalInterfaces); | ||
|
||
if (enhancedType == null) { | ||
synchronized (type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this problematic, if someone else decides to lock based on a class. Why not lock on an internal object or use a real lock?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implicit cglib cache locks on the class object already today. This is just a recreation.
You are right that this can cause problems if the class object is already locked by another thread. Using a single monitor for this would however not allow concurrently creating mocks for different classes from different threads. The question is, what limitation is preferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah your are right, both approaches have their drawbacks. Could you document the rationale for this decision in a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically, people create a single mock type per class. The alternative where Byte Buddy would need to create multiple mock types for mocking a type plus additional (explicitly specified) interfaces is rather the exception. Given that most mock types derive of a single type, the application is lock-free for these scenarios. Covering the 99% scenario lock free is probably a sane approach. Also, this requires locking only a single monitor such that dead locks are impossible (live locks are however still possible).
Locking on the class loader is more likely to cause problems, though, as class loaders typically lock on themselves during class loading. Creating a mock can cause class loading as using the reflection API (for finding methods to override) causes eager resolution of a type where all types references in methods are loaded. This is typically not a problem but can be one with parallel capable class loaders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, but I meant a short source code comment why we lock on class an not on an internal lock. Just to document that this is intentional and why.
@szpak valid points
IMHO mixing implementations would make it more complicated.
As long as we support it we need to test it. As for how, I think it should be possible with gradle to reuse the test sources and have two test projects with different dependency sets. Or maybe even in the same project using two test configurations (I need to refresh my gradle-fu). |
} else if (cglibAvailable) { | ||
proxy = CglibMockFactory.createMock(mockType, additionalInterfaces, | ||
constructorArgs, mockInterceptor, classLoader, useObjenesis); | ||
} else { | ||
throw new CannotCreateMockException(mockType, | ||
". Mocking of non-interface types requires the CGLIB library. Please put cglib-nodep-2.2 or higher on the class path." | ||
". Mocking of non-interface types requires the CGLIB library. Please put Byte Buddy (at least 1.4.0) or cglib-nodep-2.2 or higher on the class path." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really your issue but if you are already touching it. The message should say that we need at least version 3.2 of cglib.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed it.
As an additional incentive to merge this: Byte Buddy supports Java 9 by now, cglib does not (yet?). ;) |
Yes, as soon as the testing issue is resolved. We need tests to run against both cglib and ByteBuddy. |
I added a property I am not a Gradle person. Were you waiting for me to add support for testing both? I would need to read up my Gradle 101 to do this. |
Learning Gradle could be beneficial for you, @raphw ;) However, if it's not the first point on your TODO list I would be probably able to get it a try once finishing my holiday season (probably somewhere at the edge of September and October ;) ). Luckily, someone else will be able to handle it earlier. |
@szpak @leonard84 I recently had the pleasure of writing a Byte Buddy plugin for Gradle and got to know the build system a bit better. I went for a simple solution where running Gradle with: gradle test -DuseCglib=true runs all tests with cglib rather then Byte Buddy. From the comments in your .travis.yml, it does however seem like you do not want additional builds. Furthermore, AppVeyor does not seem to even attempt building the project. Neither do I understand why Travis does no longer build the branch. |
@raphw my approach would be to define another |
@leonard84 Would you know how to do that? I googled a bit, but there seems no easy way of duplicating a task in Gradle. |
If you look at https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/ you can see how to define another test task. In your case I think it would suffice to just set the SystemProperty and maybe use another output directory for the reports. |
@leonard84 Thanks for the link. I followed the recommendation and have a |
Thanks @raphw! LGTM |
thanks @raphw you did all what is neccessary as far as I can see, the reason why we haven't merged yet is that it now doubles our test-time which nearly double the total build time. |
It would be possible to filter out some tests but then again, code generation is a very complex process that is hard to test in isolation. Maybe, the tests should only run on CI but not on a standard build? |
I changed the build now to only attach the Would this suffice? |
I'm for merging this… any objections? |
Ping @leonard84 @szpak - What is the state on this? |
I've already approved your changes in a review so I cannot be against :) |
Adds support for generating classes using Byte Buddy instead of cglib when Byte Buddy is found on the class path. (Other than on AppVeyor, running
:spock-specs:test
works fine on my machine.)