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

Deep Stubs Incompatible With Mocking Enum #2984

Closed
SenorPez opened this issue Apr 22, 2023 · 16 comments · Fixed by #3167
Closed

Deep Stubs Incompatible With Mocking Enum #2984

SenorPez opened this issue Apr 22, 2023 · 16 comments · Fixed by #3167

Comments

@SenorPez
Copy link

SenorPez commented Apr 22, 2023

The following code works:

@ExtendWith(MockitoExtension.class)
class BoardTest {
    @Mock
    Piece piece;

    private Board instance;

    @BeforeEach
    void setUp() {
        instance = new Board(piece, 10);
    }

    @Test
    void getCostPerSpace() {
        when(piece.getPiece()).thenReturn(PieceType.SQUARE);
        double expected = 2d;
        assertThat(instance.getCostPerSpace()).isEqualTo(expected);
    }
}

The following code fails:

@ExtendWith(MockitoExtension.class)
class BoardTest {
    @Mock(answer = RETURNS_DEEP_STUBS)
    Piece piece;

    private Board instance;

    @BeforeEach
    void setUp() {
        instance = new Board(piece, 10);
    }

    @Test
    void getCostPerSpace() {
        when(piece.getPiece().getCostPerPieceSpace()).thenReturn(2.5d);
        double expected = 2d;
        assertThat(instance.getCostPerSpace()).isEqualTo(expected);
    }
}

with the following error:

You are seeing this disclaimer because Mockito is configured to create inlined mocks.
You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.

Underlying exception : org.mockito.exceptions.base.MockitoException: Unsupported settings with this type 'com.senorpez.game.PieceType'
org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class com.senorpez.game.PieceType

If you're not sure why you're getting this error, please open an issue on GitHub.

Mockito Version: 5.3.0

@SenorPez
Copy link
Author

The working code fails if answer is changed to RETURNS_DEEP_STUBS

@AndreasTu
Copy link
Contributor

@SenorPez Can you please provide a full running sample including the referenced classes and enums.
I can't reproduce your issue.

My Test class is working as expected:

public class DeepStubReturnsEnumTest {

    @Test
    public void deep_stub_can_mock_enum_Issue_2984() {
        final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS);
        when(mock.getTestEnum()).thenReturn(TestEnum.B);
        assertThat(mock.getTestEnum()).isEqualTo(TestEnum.B);
    }

    @Test
    public void deep_stub_can_mock_enum_method_Issue_2984() {
        final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS);

        assertThat(mock.getTestEnum().getDoubleValue()).isEqualTo(0.0);

        when(mock.getTestEnum().getDoubleValue()).thenReturn(1.0);
        assertThat(mock.getTestEnum().getDoubleValue()).isEqualTo(1.0);
    }

    @Test
    public void mock_mocking_enum_Issue_2984() {
        final var mock = mock(TestClass.class);
        when(mock.getTestEnum()).thenReturn(TestEnum.B);
        assertThat(mock.getTestEnum()).isEqualTo(TestEnum.B);
        assertThat(mock.getTestEnum().getDoubleValue()).isEqualTo(2.0);
    }

    private static class TestClass {
        TestEnum getTestEnum() {
            return TestEnum.A;
        }
    }

    private enum TestEnum {
        A,
        B;

        double getDoubleValue() {
            return 2.0;
        }
    }
}

@danielqejo
Copy link

danielqejo commented Oct 10, 2023

I'm facing the same problem and happened to replicate the error.
Looks like there's something to do with abstract methods in enums:

@RunWith(MockitoJUnitRunner.class)
public class FooTest {

	@Mock(answer = Answers.RETURNS_DEEP_STUBS)
	private Clazz clazz;

	@Test
	public void test() {
		Enumeration meme = clazz.getEnumeration();
	}

	class Clazz {
		private final Enumeration enumeration;

		public Clazz(Enumeration enumeration) {
			this.enumeration = enumeration;
		}

		public Enumeration getEnumeration() {
			return enumeration;
		}
	}

	enum Enumeration {
		FIRST {
			@Override
			void doSomething() {
				System.out.println(this.name());
			}
		},
		SECOND {
			@Override
			void doSomething() {
				System.out.println(this.name());
			}
		};

		abstract void doSomething();
	}
}

Edited a few times to replicate it :x

@AndreasTu
Copy link
Contributor

@danielqejo On which version do you execute that? Mockito, ByteBuddy, Java, etc.
When I try your test case on mockito/main the test is executed without an exception.

So I can't reproduce your problem.

I am executing:

package org.mockito.internal.stubbing.answers;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class FooTest {

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private Clazz clazz;

    @Test
    public void test() {
        Enumeration meme = clazz.getEnumeration();
    }

    class Clazz {
        private final Enumeration enumeration;

        public Clazz(Enumeration enumeration) {
            this.enumeration = enumeration;
        }

        public Enumeration getEnumeration() {
            return enumeration;
        }
    }

    enum Enumeration {
        FIRST {
            @Override
            void doSomething() {
                System.out.println(this.name());
            }
        },
        SECOND {
            @Override
            void doSomething() {
                System.out.println(this.name());
            }
        };

        abstract void doSomething();
    }
}

@danielqejo
Copy link

danielqejo commented Oct 10, 2023

I'm using:

  • corretto JDK 17
  • Mockito 5.6.0
  • bytebuddy 1.14.8

@danielqejo
Copy link

Here's complete stacktrace:

org.mockito.exceptions.base.MockitoException: 
Mockito cannot mock this class: class <hidden_package>.FooTest$Enumeration.

If you're not sure why you're getting this error, please open an issue on GitHub.


Java               : 17
JVM vendor name    : Amazon.com Inc.
JVM vendor version : 17.0.8.1+8-LTS
JVM name           : OpenJDK 64-Bit Server VM
JVM version        : 17.0.8.1+8-LTS
JVM info           : mixed mode, sharing
OS name            : Linux
OS version         : 5.15.0-86-generic


You are seeing this disclaimer because Mockito is configured to create inlined mocks.
You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc.

Underlying exception : org.mockito.exceptions.base.MockitoException: Unsupported settings with this type '<hidden_package>.FooTest$Enumeration'

	at <hidden_package>.FooTest$Clazz.getEnumeration(FooTest.java:28)
	at <hidden_package>.FooTest.test(FooTest.java:17)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	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.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:55)
	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.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:100)
	at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:107)
	at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:42)
	at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: org.mockito.exceptions.base.MockitoException: Unsupported settings with this type '<hidden_package>.FooTest$Enumeration'
	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)
	... 31 more

@danielqejo
Copy link

I've also tested with JUnit 5, but no luck whatsoever.

@danielqejo
Copy link

I've been doing some tests and found out that the problem occurs at Java 17+ only.
Java 11 works just fine.

@danielqejo
Copy link

Turns out Java 17 is making enum sealed when there is an abstract method.
In org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator:356 code returns true when has at least one abstract method, but false when there is no abstract method.

@AndreasTu
Copy link
Contributor

@danielqejo Thanks for the great analysis.
I now have a feature branch for mockito, which can reproduce that issue.

But I have not yet a clue how to fix it.

The main point in the reproducer is to compile the Enum and run tests with Java 17.
If have added a new SourceSet src/test17 which compiles and executes tests with Java 17.

@AndreasTu
Copy link
Contributor

@TimvdLippe I don't think we can fix that problem here, maybe we should add a better error message that mocking of Enums since Java 15 (see sealed-classes) does not work.

The problem is, that enums compiled with Java >= 15 (I guess due to preview feature, but at least Java 17) are sealed and have the PermittedSubclasses class attribute, which can't be retransformed away with a java agent.
So as long as we "need" a subclass for an enum mock, we can't create a subclass for that, due to the PermittedSubclasses class attribute. Maybe we can somehow hijack a real enum literal class for mocking an enum in the future, but I guess this is a very big change.

Our tests are currently not failing, because we compile our test enums with Java 11.
I have a feature branch , which adds a test source set test17, which compiles tests against Java 17, and there it is failing.

@TimvdLippe
Copy link
Contributor

If this indeed does not work (@raphw can you confirm?) then indeed let's try to detect those cases when the mockmaker is invoked whether it can feasibly mock them. I don't think we should hack our way around it. Sealed is there for a purpose, so let's respect it.

@AndreasTu
Copy link
Contributor

@TimvdLippe In addition to allow the deep stub of a class returning an enum working again in the Java 17 case, we could change the deep stup code to return the first literal of the enum, if we can't mock the enum.
What do you think?

@raphw
Copy link
Member

raphw commented Nov 1, 2023

Yes, sealed classes cannot be subclassed, also with an agent, and the attribute cannot be modified. I also think that we should avoid mocking enums for this reason. Possibly, we can add a mechanism along the static method mocking facility that treats enums similarly.

AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 2, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Fixes mockito#2984
AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 2, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Fixes mockito#2984
@AndreasTu
Copy link
Contributor

Just to be clear for the future. Mockito can still mock non-abstract enums in Java 17. It only can't mock abstract enums with abstract methods, which are overridden by literals.
See test cases in my PR.

AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 2, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Fixes mockito#2984
@raphw
Copy link
Member

raphw commented Nov 2, 2023

True. We'd need to navigate and mock any of the extended classes in such a case, that would be a close enough emulation.

AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 2, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Fixes mockito#2984
AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 28, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Added spotless.gradle to also check formatting of java21 project.

Fixes mockito#2984
AndreasTu added a commit to AndreasTu/mockito that referenced this issue Nov 29, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

If a deep stub returns an abstract enum, it uses in the error
case now the first enum literal of the real enum.

Fixes mockito#2984
TimvdLippe pushed a commit that referenced this issue Nov 29, 2023
Mockito can't mock abstract enums in Java 15 or later
because they are now marked as sealed.
So Mockito reports that now with a better error message.

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

Successfully merging a pull request may close this issue.

5 participants