Skip to content

Commit

Permalink
Deep Stubs Incompatible With Mocking Enum
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AndreasTu committed Nov 2, 2023
1 parent 5c4e72c commit 10f8b9d
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 10 deletions.
47 changes: 47 additions & 0 deletions build.gradle
Expand Up @@ -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))
}
@@ -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);
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -423,15 +424,15 @@ public <T> Class<? extends T> createMockType(MockCreationSettings<T> settings) {

private <T> RuntimeException prettifyFailure(
MockCreationSettings<T> mockFeatures, Exception generationFailed) {
if (mockFeatures.getTypeToMock().isArray()) {
Class<T> 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(...))",
Expand All @@ -442,10 +443,17 @@ private <T> 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.",
Expand All @@ -455,7 +463,7 @@ private <T> 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.",
"",
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
@@ -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();
}
}
@@ -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";
}
}
}

0 comments on commit 10f8b9d

Please sign in to comment.