-
Notifications
You must be signed in to change notification settings - Fork 238
Wrong super constructor is called #582
Description
- Version of JMockit that was used: 1.44 & 1.45
We had very strange behavior:
- In a test we have a field
@Mocked javax.faces.context.PartialResponseWriter writer; - In the constructor of another class used in that test we initialize a private field like
responseWriter = new TestResponseWriter(); javax.faces.context.ResponseWriterwhich both classes extend from (one directly, one indirectly) has no explicit constructor, just the default implied no-arg constuctorjava.io.Writeron the other hand has two protected constructors, one no-arg and one with anObjectparameter that must not benull
- Now when we run our tests, we get
java.lang.NullPointerException at java.base/java.io.Writer.<init>(Writer.java:174) at javax.faces.context.ResponseWriter.<init>(ResponseWriter.java) at de.empic.web.testutils.TestResponseWriter.<init>(TestResponseWriter.java:14) - This on first look makes absolutely no sense, as there is no constructor in
ResponseWriterthat calls the one-arg constructor ofWriter. - So I naturally kicked off the debugger to look what is going on, but suddenly it didn't happen anymore then. And this constantly stays like hat, if you run without debugger it fails with NPE, if you run with debug setting (even if you don't even attach a debugger), it runs fine
So after a couple of hours of adding stdout-debugging to JMockit and digging my way through subtle differences in the output, I got to the root cause.
In mockit.internal.expectations.mocking.MockedClassModifier#visitMethod you call generateCallToSuperConstructor().
I'm not sure why this is done, I guess so that you are able to then add the call to the MockingBridge, as the super-constructor always has to be called first.
The problem is, that your code calls a random super constructor with default values according to types. I'm not sure this is the best decision, as different constructors can ensure different invariants and so calling a random one might not be the best solution. Maybe it would better to look into the actual implementation of the constructor and calling the super-constructor the real method also is calling? But even then using default values for the arguments might not be the best solution.
In my case here, after adding
System.out.println("vvvvvvvvvvvvvvvvvvvvvvvv");
for (MethodInfo methodOrConstructor : cmr.getMethods()) {
if (methodOrConstructor.isConstructor()) {
System.out.println("methodOrConstructor.desc = " + methodOrConstructor.desc);
System.out.println("methodOrConstructor.accessFlags = " + methodOrConstructor.accessFlags);
}
}
System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^");to mockit.internal.SuperConstructorCollector#findConstructor I have seen that without debug I get
vvvvvvvvvvvvvvvvvvvvvvvv
methodOrConstructor.desc = (Ljava/lang/Object;)V
methodOrConstructor.accessFlags = 4
methodOrConstructor.desc = ()V
methodOrConstructor.accessFlags = 4
^^^^^^^^^^^^^^^^^^^^^^^^
whereas with debug I get
vvvvvvvvvvvvvvvvvvvvvvvv
methodOrConstructor.desc = ()V
methodOrConstructor.accessFlags = 4
methodOrConstructor.desc = (Ljava/lang/Object;)V
methodOrConstructor.accessFlags = 4
^^^^^^^^^^^^^^^^^^^^^^^^
and the code just takes the first one it finds.
In my case without debug this was the one-arg constructor that was given null as parameter and thus produced that NPE.
So one problem is, that it sometimes uses one constructor, sometimes the other, another problem probably is that actually it uses some random constructor that might have requirements or produces certain invariants.
