Skip to content

Commit

Permalink
Add annotation to mark a type as DoNotMock
Browse files Browse the repository at this point in the history
Mocking types that users not own [1] or are severely complicating test
logic [2] leads to brittle or wrong tests. In particular, the
StackOverflow answer is wrong, as the contract of java.util.Map is
violated. When a new key is added to the Map, the stubbed return would be wrong.
In Google we have used the DoNotMock annotation via ErrorProne [3]
to annotate these types, as well as an internal list of types that can't
be mocked (this includes several java.util types). We are using a custom
Mockmaker to enforce this on run-time.

Based on our successful experience with DoNotMock (we have seen a large
reduction in bad/broken tests for types involved), we are proposing to
open source this into Mockito itself.

The DoNotMock annotation can be added to any type, e.g. classes and
interfaces. If, in the type hierarchy of the class-to-be-mocked, there
is a type that is annotated with DoNotMock, Mockito will throw a
DoNotMockException.

This would help preventing issues such as #1827 and #1734 which is
in-line with the guidance on our wiki [1]. A follow-up change would
allow us to define external types (like the java.util types) that can't
be mocked. (We can't add the annotation to the types, as they live in the
JDK instead.)

[1]: https://github.com/mockito/mockito/wiki/How-to-write-good-tests#dont-mock-a-type-you-dont-own
[2]: https://stackoverflow.com/a/15820143
[3]: https://errorprone.info/api/latest/com/google/errorprone/annotations/DoNotMock.html
  • Loading branch information
TimvdLippe committed Nov 28, 2019
1 parent dc6eadc commit b740f99
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/main/java/org/mockito/DoNotMock.java
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
* Annotation representing a type that should not be mocked.
* <p>When marking a type {@code @DoNotMock}, you should always point to alternative testing
* solutions such as standard fakes or other testing utilities.
*/
@Target({TYPE})
@Retention(RUNTIME)
@Documented
public @interface DoNotMock {
/**
* The reason why the annotated type should not be mocked.
*
* <p>This should suggest alternative APIs to use for testing objects of this type.
*/
String value() default "Create a real instance instead";
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.exceptions.misusing;

import org.mockito.exceptions.base.MockitoException;

/**
* Thrown when attempting to mock a class that is annotated with {@link org.mockito.DoNotMock}.
*/
public class DoNotMockException extends MockitoException {
public DoNotMockException(String message) {
super(message);
}
}
24 changes: 24 additions & 0 deletions src/main/java/org/mockito/internal/MockitoCore.java
Expand Up @@ -15,12 +15,15 @@
import static org.mockito.internal.verification.VerificationModeFactory.noInteractions;
import static org.mockito.internal.verification.VerificationModeFactory.noMoreInteractions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.mockito.DoNotMock;
import org.mockito.InOrder;
import org.mockito.MockSettings;
import org.mockito.MockingDetails;
import org.mockito.exceptions.misusing.DoNotMockException;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.internal.creation.MockSettingsImpl;
import org.mockito.internal.invocation.finder.VerifiableInvocationsFinder;
Expand Down Expand Up @@ -60,11 +63,32 @@ public <T> T mock(Class<T> typeToMock, MockSettings settings) {
}
MockSettingsImpl impl = MockSettingsImpl.class.cast(settings);
MockCreationSettings<T> creationSettings = impl.build(typeToMock);
checkDoNotMockAnnotation(creationSettings.getTypeToMock(), creationSettings);
T mock = createMock(creationSettings);
mockingProgress().mockingStarted(mock, creationSettings);
return mock;
}

private void checkDoNotMockAnnotation(Class<?> typeToMock,
MockCreationSettings<?> creationSettings) {
ArrayList<Class<?>> unmockableTypes = new ArrayList<>(
creationSettings.getExtraInterfaces());

Class<?> mockTypeToCheck = typeToMock;
while (mockTypeToCheck != null && mockTypeToCheck != Object.class) {
unmockableTypes.add(mockTypeToCheck);
Class<?>[] interfaces = mockTypeToCheck.getInterfaces();
if (interfaces != null) {
unmockableTypes.addAll(Arrays.asList(interfaces));
}
mockTypeToCheck = mockTypeToCheck.getSuperclass();
}

if (unmockableTypes.stream().anyMatch(clazz -> clazz.isAnnotationPresent(DoNotMock.class))) {
throw new DoNotMockException(creationSettings.getTypeToMock() + " is annotated with @DoNoMock and can't be mocked.");
}
}

public <T> OngoingStubbing<T> when(T methodCall) {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
Expand Down
73 changes: 73 additions & 0 deletions src/test/java/org/mockitousage/annotation/DoNotMockTest.java
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2019 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockitousage.annotation;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;

import org.junit.Test;
import org.mockito.DoNotMock;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.exceptions.misusing.DoNotMockException;

public class DoNotMockTest {

@Test
public void can_not_mock_class_annotated_with_donotmock() {
assertThatThrownBy(() -> {
NotMockable notMockable = mock(NotMockable.class);
}).isInstanceOf(DoNotMockException.class);
}

@Test
public void can_not_mock_class_via_mock_annotation() {
assertThatThrownBy(() -> {
MockitoAnnotations.initMocks(new TestClass());
}).isInstanceOf(DoNotMockException.class);
}

@Test
public void can_not_mock_class_with_interface_annotated_with_donotmock() {
assertThatThrownBy(() -> {
SubclassOfNotMockableInterface notMockable = mock(SubclassOfNotMockableInterface.class);
}).isInstanceOf(DoNotMockException.class);
}

@Test
public void can_not_mock_subclass_with_unmockable_interface() {
assertThatThrownBy(() -> {
DoubleSubclassOfInterface notMockable = mock(DoubleSubclassOfInterface.class);
}).isInstanceOf(DoNotMockException.class);
}

@Test
public void can_not_mock_subclass_with_unmockable_superclass() {
assertThatThrownBy(() -> {
SubclassOfNotMockableSuperclass notMockable = mock(SubclassOfNotMockableSuperclass.class);
}).isInstanceOf(DoNotMockException.class);
}

@DoNotMock
private static class NotMockable {

}

@DoNotMock
private interface NotMockableInterface {

}

static class SubclassOfNotMockableInterface implements NotMockableInterface {}

private static class DoubleSubclassOfInterface extends SubclassOfNotMockableInterface {}

private static class SubclassOfNotMockableSuperclass extends NotMockable {}

private static class TestClass {
@Mock private NotMockable notMockable;
}

}

0 comments on commit b740f99

Please sign in to comment.