Skip to content

Commit

Permalink
Throw error when mocking sealed abstract enum (#3167)
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.

Fixes #2984
  • Loading branch information
AndreasTu authored Nov 29, 2023
1 parent 290a8e1 commit 74633b8
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -425,15 +425,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 @@ -444,10 +444,18 @@ private <T> RuntimeException prettifyFailure(
"Underlying exception : " + generationFailed),
generationFailed);
}
if (Modifier.isPrivate(mockFeatures.getTypeToMock().getModifiers())) {
if (TypeSupport.INSTANCE.isSealed(typeToMock) && typeToMock.isEnum()) {
throw new MockitoException(
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.",
"You can still return an existing enum literal from a stubbed method call."),
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 @@ -457,7 +465,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
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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.*;

import org.junit.Test;
import org.mockito.exceptions.base.MockitoException;

public class DeepStubReturnsEnumJava21Test {

@Test
public void cant_mock_enum_class_in_Java21_Issue_2984() {
assertThatThrownBy(
() -> {
mock(TestEnum.class);
})
.isInstanceOf(MockitoException.class)
.hasMessageContaining("Sealed abstract enums can't be mocked.")
.hasCauseInstanceOf(MockitoException.class);
}

@Test
public void cant_mock_enum_class_as_deep_stub_in_Java21_Issue_2984() {
assertThatThrownBy(
() -> {
mock(TestEnum.class, RETURNS_DEEP_STUBS);
})
.isInstanceOf(MockitoException.class)
.hasMessageContaining("Sealed abstract enums can't be mocked.")
.hasCauseInstanceOf(MockitoException.class);
}

@Test
public void deep_stub_cant_mock_enum_with_abstract_method_in_Java21_Issue_2984() {
final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS);
assertThatThrownBy(
() -> {
mock.getTestEnum();
})
.isInstanceOf(MockitoException.class)
.hasMessageContaining("Sealed abstract enums can't be mocked.")
.hasCauseInstanceOf(MockitoException.class);
}

@Test
public void deep_stub_can_override_mock_enum_with_abstract_method_in_Java21_Issue_2984() {
final var mock = mock(TestClass.class, RETURNS_DEEP_STUBS);
// We need the doReturn() because when calling when(mock.getTestEnum()) it will already
// throw an exception.
doReturn(TestEnum.A).when(mock).getTestEnum();

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_Java21_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_Java21_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 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 74633b8

Please sign in to comment.