diff --git a/build.gradle b/build.gradle index 996947cd56..495fa0b4df 100644 --- a/build.gradle +++ b/build.gradle @@ -140,3 +140,50 @@ spotless { targetExclude 'src/test/java/org/mockitousage/internal/junit/UnusedStubbingsFinderTest.java' } } + + +/* + * SourceSet for Java 17 tests + */ +sourceSets { + test17 { + java { + srcDir("src/test17/java") + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + compileClasspath += sourceSets.test.output + runtimeClasspath += sourceSets.test.output + } + } +} + +configurations { + test17Implementation.extendsFrom(testImplementation) + test17CompileOnly.extendsFrom(testCompileOnly) + test17RuntimeOnly.extendsFrom(testRuntimeOnly) +} + +tasks.named("compileTest17Java", JavaCompile).configure{ + options.release.set(17) + javaCompiler.set(javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + }) +} + +def test17 = tasks.register("test17", Test){ + useJUnit() + testClassesDirs = sourceSets.test17.output.classesDirs + classpath = sourceSets.test17.runtimeClasspath + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + }) +} + +tasks.named("check").configure { + dependsOn(test17) +} + +tasks.named("mockitoCoverage", JacocoReport).configure {mockitoCoverage -> + mockitoCoverage.executionData(project.files(test17.get().jacoco.destinationFile).builtBy(test17)) +} diff --git a/src/main/java/org/mockito/exceptions/base/MockitoCantMockEnumException.java b/src/main/java/org/mockito/exceptions/base/MockitoCantMockEnumException.java new file mode 100644 index 0000000000..2170b6252c --- /dev/null +++ b/src/main/java/org/mockito/exceptions/base/MockitoCantMockEnumException.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.exceptions.base; + +/** + * Raised by mockito to emit an error when an {@link Enum} shall be mocked, but the Java Runtime does not permit it. + */ +public class MockitoCantMockEnumException extends MockitoException { + + private static final long serialVersionUID = 1L; + + public MockitoCantMockEnumException(String message, Throwable t) { + super(message, t); + } +} diff --git a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java index 227df4cd15..cf93e83c57 100644 --- a/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java +++ b/src/main/java/org/mockito/internal/creation/bytebuddy/InlineDelegateByteBuddyMockMaker.java @@ -8,6 +8,7 @@ import org.mockito.MockedConstruction; import org.mockito.creation.instance.InstantiationException; import org.mockito.creation.instance.Instantiator; +import org.mockito.exceptions.base.MockitoCantMockEnumException; import org.mockito.exceptions.base.MockitoException; import org.mockito.exceptions.base.MockitoInitializationException; import org.mockito.exceptions.misusing.MockitoConfigurationException; @@ -423,15 +424,15 @@ public Class createMockType(MockCreationSettings settings) { private RuntimeException prettifyFailure( MockCreationSettings mockFeatures, Exception generationFailed) { - if (mockFeatures.getTypeToMock().isArray()) { + Class typeToMock = mockFeatures.getTypeToMock(); + if (typeToMock.isArray()) { throw new MockitoException( - join("Arrays cannot be mocked: " + mockFeatures.getTypeToMock() + ".", ""), - generationFailed); + join("Arrays cannot be mocked: " + typeToMock + ".", ""), generationFailed); } - if (Modifier.isFinal(mockFeatures.getTypeToMock().getModifiers())) { + if (Modifier.isFinal(typeToMock.getModifiers())) { throw new MockitoException( join( - "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", + "Mockito cannot mock this class: " + typeToMock + ".", "Can not mock final classes with the following settings :", " - explicit serialization (e.g. withSettings().serializable())", " - extra interfaces (e.g. withSettings().extraInterfaces(...))", @@ -442,10 +443,17 @@ private RuntimeException prettifyFailure( "Underlying exception : " + generationFailed), generationFailed); } - if (Modifier.isPrivate(mockFeatures.getTypeToMock().getModifiers())) { + if (TypeSupport.INSTANCE.isSealed(typeToMock) && typeToMock.isEnum()) { + throw new MockitoCantMockEnumException( + join( + "Mockito cannot mock this class: " + typeToMock + ".", + "Sealed abstract enums can't be mocked. Since Java 15 abstract enums are declared sealed, which prevents mocking."), + generationFailed); + } + if (Modifier.isPrivate(typeToMock.getModifiers())) { throw new MockitoException( join( - "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", + "Mockito cannot mock this class: " + typeToMock + ".", "Most likely it is a private class that is not visible by Mockito", "", "You are seeing this disclaimer because Mockito is configured to create inlined mocks.", @@ -455,7 +463,7 @@ private RuntimeException prettifyFailure( } throw new MockitoException( join( - "Mockito cannot mock this class: " + mockFeatures.getTypeToMock() + ".", + "Mockito cannot mock this class: " + typeToMock + ".", "", "If you're not sure why you're getting this error, please open an issue on GitHub.", "", diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java index 27faed9d9a..143287233b 100644 --- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java +++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java @@ -12,6 +12,7 @@ import org.mockito.MockSettings; import org.mockito.Mockito; +import org.mockito.exceptions.base.MockitoCantMockEnumException; import org.mockito.internal.MockitoCore; import org.mockito.internal.creation.settings.CreationSettings; import org.mockito.internal.stubbing.InvocationContainerImpl; @@ -76,8 +77,16 @@ public Object answer(InvocationOnMock invocation) throws Throwable { if (rawType.equals(Object.class) && !returnTypeGenericMetadata.hasRawExtraInterfaces()) { return null; } - - return deepStub(invocation, returnTypeGenericMetadata); + try { + return deepStub(invocation, returnTypeGenericMetadata); + } catch (MockitoCantMockEnumException ex) { + // If the enum is not mockable, e.g. due to sealed, return the first literal as value + Object[] enumConstants = rawType.getEnumConstants(); + if (enumConstants == null || enumConstants.length == 0) { + return null; + } + return enumConstants[0]; + } } private Object deepStub( diff --git a/src/test/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava11Test.java b/src/test/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava11Test.java new file mode 100644 index 0000000000..70b7b54968 --- /dev/null +++ b/src/test/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava11Test.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.stubbing.answers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +public class DeepStubReturnsEnumJava11Test { + private static final String MOCK_VALUE = "Mock"; + + @Test + public void deep_stub_can_mock_enum_getter_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_class_Issue_2984() { + final var mock = mock(TestEnum.class, RETURNS_DEEP_STUBS); + when(mock.getValue()).thenReturn(MOCK_VALUE); + assertThat(mock.getValue()).isEqualTo(MOCK_VALUE); + } + + @Test + public void deep_stub_can_mock_enum_method_Issue_2984() { + final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS); + assertThat(mock.getTestEnum().getValue()).isEqualTo(null); + + when(mock.getTestEnum().getValue()).thenReturn(MOCK_VALUE); + assertThat(mock.getTestEnum().getValue()).isEqualTo(MOCK_VALUE); + } + + @Test + public void mock_mocking_enum_getter_Issue_2984() { + final var mock = mock(TestClass.class); + when(mock.getTestEnum()).thenReturn(TestEnum.B); + assertThat(mock.getTestEnum()).isEqualTo(TestEnum.B); + assertThat(mock.getTestEnum().getValue()).isEqualTo("B"); + } + + static class TestClass { + TestEnum getTestEnum() { + return TestEnum.A; + } + } + + enum TestEnum { + A { + @Override + String getValue() { + return this.name(); + } + }, + B { + @Override + String getValue() { + return this.name(); + } + }, + ; + + abstract String getValue(); + } +} diff --git a/src/test17/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava17Test.java b/src/test17/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava17Test.java new file mode 100644 index 0000000000..e52939d66a --- /dev/null +++ b/src/test17/java/org/mockito/internal/stubbing/answers/DeepStubReturnsEnumJava17Test.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.stubbing.answers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.exceptions.base.MockitoCantMockEnumException; +import org.mockito.exceptions.base.MockitoException; + +public class DeepStubReturnsEnumJava17Test { + + @Test + public void cant_mock_enum_class_in_Java17_Issue_2984() { + assertThatThrownBy( + () -> { + mock(TestEnum.class); + }) + .isInstanceOf(MockitoCantMockEnumException.class) + .hasMessage( + "\nMockito cannot mock this class: class org.mockito.internal.stubbing.answers.DeepStubReturnsEnumJava17Test$TestEnum.\n" + + "Sealed abstract enums can't be mocked. Since Java 15 abstract enums are declared sealed, which prevents mocking.") + .hasCauseInstanceOf(MockitoException.class); + } + + @Test + public void cant_mock_enum_class_as_deep_stub_in_Java17_Issue_2984() { + assertThatThrownBy( + () -> { + mock(TestEnum.class, RETURNS_DEEP_STUBS); + }) + .isInstanceOf(MockitoCantMockEnumException.class) + .hasMessage( + "\nMockito cannot mock this class: class org.mockito.internal.stubbing.answers.DeepStubReturnsEnumJava17Test$TestEnum.\n" + + "Sealed abstract enums can't be mocked. Since Java 15 abstract enums are declared sealed, which prevents mocking.") + .hasCauseInstanceOf(MockitoException.class); + } + + @Test + public void deep_stub_cant_mock_enum_with_abstract_method_in_Java17_Issue_2984() { + final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS); + // It returns the value of the first enum literal, because it is not mockable + assertThat(mock.getTestEnum()).isEqualTo(TestEnum.A); + assertThat(mock.getTestEnum().getValue()).isEqualTo("A"); + + assertThat(mockingDetails(mock.getTestEnum()).isMock()).isFalse(); + } + + @Test + public void deep_stub_can_mock_enum_without_method_in_Java17_Issue_2984() { + final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS); + assertThat(mock.getTestNonAbstractEnum()).isNotNull(); + + assertThat(mockingDetails(mock.getTestNonAbstractEnum()).isMock()).isTrue(); + when(mock.getTestNonAbstractEnum()).thenReturn(TestNonAbstractEnum.B); + assertThat(mock.getTestNonAbstractEnum()).isEqualTo(TestNonAbstractEnum.B); + } + + @Test + public void deep_stub_can_mock_enum_without_abstract_method_in_Java17_Issue_2984() { + final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS); + assertThat(mock.getTestNonAbstractEnumWithMethod()).isNotNull(); + assertThat(mock.getTestNonAbstractEnumWithMethod().getValue()).isNull(); + assertThat(mockingDetails(mock.getTestNonAbstractEnumWithMethod()).isMock()).isTrue(); + + when(mock.getTestNonAbstractEnumWithMethod().getValue()).thenReturn("Mock"); + assertThat(mock.getTestNonAbstractEnumWithMethod().getValue()).isEqualTo("Mock"); + + when(mock.getTestNonAbstractEnumWithMethod()).thenReturn(TestNonAbstractEnumWithMethod.B); + assertThat(mock.getTestNonAbstractEnumWithMethod()) + .isEqualTo(TestNonAbstractEnumWithMethod.B); + } + + @Test + public void deep_stub_can_mock_enum_getter_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 mock_mocking_enum_getter_Issue_2984() { + final var mock = mock(TestClass.class); + when(mock.getTestEnum()).thenReturn(TestEnum.B); + assertThat(mock.getTestEnum()).isEqualTo(TestEnum.B); + assertThat(mock.getTestEnum().getValue()).isEqualTo("B"); + } + + static class TestClass { + TestEnum getTestEnum() { + return TestEnum.A; + } + + TestNonAbstractEnumWithMethod getTestNonAbstractEnumWithMethod() { + return TestNonAbstractEnumWithMethod.A; + } + + TestNonAbstractEnum getTestNonAbstractEnum() { + return TestNonAbstractEnum.A; + } + } + + enum TestEnum { + A { + @Override + String getValue() { + return this.name(); + } + }, + B { + @Override + String getValue() { + return this.name(); + } + }, + ; + + abstract String getValue(); + } + + enum TestNonAbstractEnum { + A, + B + } + + enum TestNonAbstractEnumWithMethod { + A, + B; + + String getValue() { + return "RealValue"; + } + } +}