quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.88k stars 2.71k forks source link

Mockito fails to mock non-public inner class in continuous testing due to classloading issues #38987

Open famod opened 9 months ago

famod commented 9 months ago

Describe the bug

The following primitive test (and tested class):

@ExtendWith(MockitoExtension.class)
public class FooTest {

    @Mock
    private Foo.Inner mock;

    @Test
    public void test() {

    }
}
public class Foo {

    static class Inner {
    }
}

works fine in IDE or mvn, but fails with mvn quarkus:test:

ERROR [io.qua.test] (Test runner thread) Test FooTest#test() failed 
: org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class org.acme.Foo$Inner.

Mockito can only mock non-private & non-final classes, but the root cause of this error might be different.
Please check the full stacktrace to understand what the issue is.
If you're still not sure why you're getting this error, please open an issue on GitHub.

Java               : 17
JVM vendor name    : Azul Systems, Inc.
JVM vendor version : 17.0.9+8-LTS
JVM name           : OpenJDK 64-Bit Server VM
JVM version        : 17.0.9+8-LTS
JVM info           : mixed mode, emulated-client, sharing
OS name            : Linux
OS version         : 5.15.0-94-generic

Underlying exception : org.mockito.exceptions.base.MockitoException: 
Cannot create mock for class org.acme.Foo$Inner

The type is not public and its mock class is loaded by a different class loader.
This can have multiple reasons:
 - You are mocking a class with additional interfaces of another class loader
 - Mockito is loaded by a different class loader than the mocked type (e.g. with OSGi)
 - The thread's context class loader is different than the mock's class loader
    at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:160)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    Suppressed: java.lang.NullPointerException: Cannot invoke "java.util.Set.forEach(java.util.function.Consumer)" because the return value of "org.junit.jupiter.api.extension.ExtensionContext$Store.remove(Object, java.lang.Class)" is null
        at org.mockito.junit.jupiter.MockitoExtension.afterEach(MockitoExtension.java:194)
        ... 2 more
Caused by: org.mockito.exceptions.base.MockitoException: 
Cannot create mock for class org.acme.Foo$Inner

The type is not public and its mock class is loaded by a different class loader.
This can have multiple reasons:
 - You are mocking a class with additional interfaces of another class loader
 - Mockito is loaded by a different class loader than the mocked type (e.g. with OSGi)
 - The thread's context class loader is different than the mock's class loader
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:399)
    at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:190)
    at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:410)
    ... 3 more

ERROR [io.qua.test] (Test runner thread) >>>>>>>>>>>>>>>>>>>> Summary: <<<<<<<<<<<<<<<<<<<<
FooTest#test()

Expected behavior

No failure, should work as in IDE or mvn.

Actual behavior

Fails with classloading issue.

How to Reproduce?

  1. clone https://github.com/famod/q_ctest-inner
  2. mvn clean verify (passes)
  3. mvn quarkus:test fails

Output of uname -a or ver

No response

Output of java -version

No response

Quarkus version or git rev

3.7.4

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

The problem vanishes after adding public to the inner class.

I have another case where adding public helps, but that's not an inner class.

It has been an issue for many releases now, I just haven't found the time to report it. I'm rather sure (IIRC) it was actually working some months ago.

quarkus-bot[bot] commented 9 months ago

/cc @geoand (testing), @stuartwdouglas (continuous-testing)

famod commented 9 months ago

I just found out it worked with 3.1.3.Final and broke with 3.2.0.CR1.

famod commented 9 months ago

Reverting https://github.com/quarkusio/quarkus/commit/13103ee74ae88b8ad4610057df127e485389fb9a fixes it! So for this to work, the TCCL has to be set before every test.

It also fixes my non-inner class case (which had other issues after adding public).

/cc @stuartwdouglas

famod commented 9 months ago

The mentioned commit has this note:

This prevents extensions from modifying the TCCL. The correct TCCL is set at the start of the run.

I'm wondering whether we had any concrete issues with specific extensions. The PR doesn't link anything in that regard.

/cc @geoand Also /cc @holly-cummins, I've seen you fighting various test classloading related issues in the past. :slightly_smiling_face:

geoand commented 9 months ago

I'm wondering whether we had any concrete issues with specific extensions. The PR doesn't link anything in that regard.

Yeah, I am wondering the same...

famod commented 9 months ago

FWIW, reverting causes a couple of test failures in ComponentContinuousTestingTest (which I haven't looked at in detail yet).

famod commented 8 months ago

FWIW, reverting causes a couple of test failures in ComponentContinuousTestingTest (which I haven't looked at in detail yet).

I think this is the root cause of that test failure (after the revert):

java.lang.IllegalArgumentException: Annotation is not a registered qualifier: interface jakarta.enterprise.inject.Any
    at io.quarkus.arc.impl.Qualifiers.verifyQualifier(Qualifiers.java:152)
    at io.quarkus.arc.impl.Qualifiers.verify(Qualifiers.java:47)
    at io.quarkus.arc.impl.ArcContainerImpl.resolveObserverMethods(ArcContainerImpl.java:881)
    at io.quarkus.arc.impl.EventImpl.createNotifier(EventImpl.java:182)
    at io.quarkus.arc.impl.ArcContainerImpl.notifierOrNull(ArcContainerImpl.java:513)
    at io.quarkus.arc.impl.ArcContainerImpl.<init>(ArcContainerImpl.java:206)
    at io.quarkus.arc.Arc.initialize(Arc.java:38)
    at io.quarkus.arc.Arc.initialize(Arc.java:22)
    at io.quarkus.test.component.QuarkusComponentTestExtension.startContainer(QuarkusComponentTestExtension.java:401)
    at io.quarkus.test.component.QuarkusComponentTestExtension.beforeEach(QuarkusComponentTestExtension.java:231)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

/cc @mkouba

mkouba commented 8 months ago

FWIW, reverting causes a couple of test failures in ComponentContinuousTestingTest (which I haven't looked at in detail yet).

I think this is the root cause of that test failure (after the revert):

java.lang.IllegalArgumentException: Annotation is not a registered qualifier: interface jakarta.enterprise.inject.Any
  at io.quarkus.arc.impl.Qualifiers.verifyQualifier(Qualifiers.java:152)
  at io.quarkus.arc.impl.Qualifiers.verify(Qualifiers.java:47)
  at io.quarkus.arc.impl.ArcContainerImpl.resolveObserverMethods(ArcContainerImpl.java:881)
  at io.quarkus.arc.impl.EventImpl.createNotifier(EventImpl.java:182)
  at io.quarkus.arc.impl.ArcContainerImpl.notifierOrNull(ArcContainerImpl.java:513)
  at io.quarkus.arc.impl.ArcContainerImpl.<init>(ArcContainerImpl.java:206)
  at io.quarkus.arc.Arc.initialize(Arc.java:38)
  at io.quarkus.arc.Arc.initialize(Arc.java:22)
  at io.quarkus.test.component.QuarkusComponentTestExtension.startContainer(QuarkusComponentTestExtension.java:401)
  at io.quarkus.test.component.QuarkusComponentTestExtension.beforeEach(QuarkusComponentTestExtension.java:231)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

/cc @mkouba

Well, I don't think there's something we could fix in ArC or in QuarkusComponentTest :shrug:.

famod commented 2 months ago

Hi @holly-cummins, this might be something for https://github.com/orgs/quarkusio/projects/30