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.

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

Fixes mockito#2984
  • Loading branch information
AndreasTu committed Nov 28, 2023
1 parent b6554b2 commit a72c1fd
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 33 deletions.
23 changes: 1 addition & 22 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ buildscript {
classpath 'org.shipkit:shipkit-changelog:1.2.0'
classpath 'org.shipkit:shipkit-auto-version:1.2.2'

classpath 'com.google.googlejavaformat:google-java-format:1.18.1'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21"
}
Expand Down Expand Up @@ -119,24 +118,4 @@ animalsniffer {
]
}

spotless {
// We run the check separately on CI, so don't run this by default
enforceCheck = false

java {
licenseHeaderFile rootProject.file('config/spotless/spotless.header')

custom 'google-java-format', { source ->
com.google.googlejavaformat.java.JavaFormatterOptions options = new com.google.googlejavaformat.java.AutoValue_JavaFormatterOptions.Builder()
.style(com.google.googlejavaformat.java.JavaFormatterOptions.Style.AOSP)
.formatJavadoc(false)
.reorderModifiers(true)
.build()
com.google.googlejavaformat.java.Formatter formatter = new com.google.googlejavaformat.java.Formatter(options)
return formatter.formatSource(source)
}

// This test contains emulation of same-line stubbings. The formatter would put them on a separate line.
targetExclude 'src/test/java/org/mockitousage/internal/junit/UnusedStubbingsFinderTest.java'
}
}
apply from: 'gradle/spotless.gradle'
33 changes: 33 additions & 0 deletions gradle/spotless.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
buildscript {
repositories {
gradlePluginPortal()
}

dependencies {
classpath 'com.google.googlejavaformat:google-java-format:1.18.1'
}
}

apply plugin: 'com.diffplug.spotless'

spotless {
// We run the check separately on CI, so don't run this by default
enforceCheck = false

java {
licenseHeaderFile rootProject.file('config/spotless/spotless.header')

custom 'google-java-format', { source ->
com.google.googlejavaformat.java.JavaFormatterOptions options = new com.google.googlejavaformat.java.AutoValue_JavaFormatterOptions.Builder()
.style(com.google.googlejavaformat.java.JavaFormatterOptions.Style.AOSP)
.formatJavadoc(false)
.reorderModifiers(true)
.build()
com.google.googlejavaformat.java.Formatter formatter = new com.google.googlejavaformat.java.Formatter(options)
return formatter.formatSource(source)
}

// This test contains emulation of same-line stubbings. The formatter would put them on a separate line.
targetExclude 'src/test/java/org/mockitousage/internal/junit/UnusedStubbingsFinderTest.java'
}
}
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();
}
}
2 changes: 2 additions & 0 deletions subprojects/java21/java21.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
apply from: "$rootDir/gradle/dependencies.gradle"
apply from: "$rootDir/gradle/java-test.gradle"
apply from: "$rootDir/gradle/spotless.gradle"


description = "Test suite for Java 21 Mockito"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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)
.hasMessage(
"\nMockito cannot mock this class: class org.mockito.internal.stubbing.answers.DeepStubReturnsEnumJava21Test$TestEnum.\n"
+ "Sealed abstract enums can't be mocked. Since Java 15 abstract enums are declared sealed, which prevents mocking.\n"
+ "You can still return an existing enum literal from a stubbed method call.")
.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)
.hasMessage(
"\nMockito cannot mock this class: class org.mockito.internal.stubbing.answers.DeepStubReturnsEnumJava21Test$TestEnum.\n"
+ "Sealed abstract enums can't be mocked. Since Java 15 abstract enums are declared sealed, which prevents mocking.\n"
+ "You can still return an existing enum literal from a stubbed method call.")
.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.");
}

@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";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
public class RecordTest {

@Test
public void given_list_is_already_spied__when_spying_record__then_record_fields_are_correctly_populated() {
public void
given_list_is_already_spied__when_spying_record__then_record_fields_are_correctly_populated() {
var ignored = spy(List.of());

record MyRecord(String name) {
}
record MyRecord(String name) {}
MyRecord spiedRecord = spy(new MyRecord("something"));

assertThat(spiedRecord.name()).isEqualTo("something");
Expand Down

0 comments on commit a72c1fd

Please sign in to comment.